From a3537c8b5341bbc805e9aabc784aa3536601363b Mon Sep 17 00:00:00 2001 From: Dylan Kilgore Date: Wed, 23 Aug 2023 11:41:31 -0700 Subject: [PATCH 1/2] feat: pickers: changeonblur and button props with option to hide buttons --- .../DatePicker/DatePicker.stories.tsx | 77 ++++++ .../DatePicker/Generate/Generate.tsx | 4 + .../Generate/generateRangePicker.tsx | 14 + .../Generate/generateSinglePicker.tsx | 19 +- .../Internal/Hooks/useCellClassNames.ts | 4 +- .../Internal/Hooks/usePickerInput.ts | 18 +- .../Internal/Hooks/useRangeDisabled.ts | 37 ++- .../Internal/Hooks/useRangeOpen.ts | 125 +++++++++ .../DateTimePicker/Internal/OcPicker.tsx | 67 +++-- .../DateTimePicker/Internal/OcPicker.types.ts | 103 ++++++- .../Internal/OcPickerPartial.tsx | 23 +- .../DateTimePicker/Internal/OcRangePicker.tsx | 255 +++++++++--------- .../Partials/DatePartial/Date.types.ts | 5 + .../Partials/DatePartial/DateBody.tsx | 2 + .../Partials/DatetimePartial/Datetime.tsx | 4 + .../Partials/TimePartial/Time.types.ts | 13 + .../Internal/Tests/blur.test.tsx | 126 +++++++++ .../Internal/Tests/keyboard.test.tsx | 18 +- .../Internal/Tests/picker.test.tsx | 57 ++-- .../Internal/Tests/range.test.tsx | 81 +++--- .../Internal/Tests/util/commonUtil.tsx | 72 +++++ .../DateTimePicker/Internal/Utils/dateUtil.ts | 3 +- .../Internal/Utils/getRanges.tsx | 42 ++- .../TimePicker/TimePicker.stories.tsx | 14 +- src/hooks/useEvent.ts | 15 ++ 25 files changed, 930 insertions(+), 268 deletions(-) create mode 100644 src/components/DateTimePicker/Internal/Hooks/useRangeOpen.ts create mode 100644 src/components/DateTimePicker/Internal/Tests/blur.test.tsx create mode 100644 src/hooks/useEvent.ts diff --git a/src/components/DateTimePicker/DatePicker/DatePicker.stories.tsx b/src/components/DateTimePicker/DatePicker/DatePicker.stories.tsx index 7084f387a..d85660585 100644 --- a/src/components/DateTimePicker/DatePicker/DatePicker.stories.tsx +++ b/src/components/DateTimePicker/DatePicker/DatePicker.stories.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import dayjs from 'dayjs'; import type { Dayjs } from 'dayjs'; import { Stories } from '@storybook/addon-docs'; +import { ButtonVariant } from '../../Button'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import DatePicker from './'; import { DatePickerShape, DatePickerSize } from './'; @@ -43,6 +44,10 @@ export default { options: [true, false], control: { type: 'inline-radio' }, }, + changeOnBlur: { + options: [true, false], + control: { type: 'inline-radio' }, + }, disabled: { options: [true, false], control: { type: 'inline-radio' }, @@ -174,6 +179,20 @@ const Single_Picker_Choose_Time_Story: ComponentStory = ( return ; }; +const Single_Picker_Choose_Time_Hide_Buttons_Story: ComponentStory< + typeof DatePicker +> = (args) => { + const onChange = ( + value: DatePickerProps['value'], + dateString: [string, string] | string + ) => { + console.log('Selected Time: ', value); + console.log('Formatted Selected Time: ', dateString); + }; + + return ; +}; + const { RangePicker } = DatePicker; const Range_Picker_Story: ComponentStory = (args) => { @@ -214,6 +233,27 @@ const Range_Picker_Choose_Time_Story: ComponentStory = ( ); }; +const Range_Picker_Choose_Time_Hide_Buttons_Story: ComponentStory< + typeof RangePicker +> = (args) => { + const onChange = ( + value: RangePickerProps['value'], + dateString: [string, string] | string + ) => { + console.log('Selected Time: ', value); + console.log('Formatted Selected Time: ', dateString); + }; + + return ( + + ); +}; + const Range_Picker_Disabled_Story: ComponentStory = ( args ) => { @@ -478,11 +518,15 @@ export const Single_Picker_Disabled_Date_and_Time = export const Single_Picker_Choose_Time = Single_Picker_Choose_Time_Story.bind( {} ); +export const Single_Picker_Choose_Time_Hide_Buttons = + Single_Picker_Choose_Time_Hide_Buttons_Story.bind({}); export const Range_Picker = Range_Picker_Story.bind({}); export const Range_Picker_Disabled = Range_Picker_Disabled_Story.bind({}); export const Range_Picker_Disabled_Date_and_Time = Range_Picker_Disabled_Date_and_Time_Story.bind({}); export const Range_Picker_Choose_Time = Range_Picker_Choose_Time_Story.bind({}); +export const Range_Picker_Choose_Time_Hide_Buttons = + Range_Picker_Choose_Time_Hide_Buttons_Story.bind({}); export const Preset_Ranges = Preset_Ranges_Story.bind({}); export const Select_Range_By_Day_Limit = Select_Range_By_Day_Limit_Story.bind( {} @@ -504,10 +548,12 @@ export const __namedExportsOrder = [ 'Single_Picker_Disabled', 'Single_Picker_Disabled_Date_and_Time', 'Single_Picker_Choose_Time', + 'Single_Picker_Choose_Time_Hide_Buttons', 'Range_Picker', 'Range_Picker_Disabled', 'Range_Picker_Disabled_Date_and_Time', 'Range_Picker_Choose_Time', + 'Range_Picker_Choose_Time_Hide_Buttons', 'Preset_Ranges', 'Select_Range_By_Day_Limit', 'Date_Format_Basic', @@ -526,6 +572,14 @@ const pickerArgs: Object = { popupPlacement: 'bottomLeft', shape: DatePickerShape.Rectangle, size: DatePickerSize.Medium, + changeOnBlur: true, + nowButtonProps: null, + okButtonProps: { variant: ButtonVariant.Primary }, + showNow: true, + showOk: true, + showToday: true, + todayActive: true, + todayButtonProps: null, }; Single_Picker.args = { @@ -545,29 +599,48 @@ Single_Picker_Choose_Time.args = { ...pickerArgs, }; +Single_Picker_Choose_Time_Hide_Buttons.args = { + ...pickerArgs, + showNow: false, + showOk: false, +}; + Range_Picker.args = { ...pickerArgs, + showToday: false, // The range picker default is false, this is for Storybook args only. }; Range_Picker_Disabled.args = { ...pickerArgs, + showToday: false, disabled: true, }; Range_Picker_Disabled_Date_and_Time.args = { ...pickerArgs, + showToday: false, }; Range_Picker_Choose_Time.args = { ...pickerArgs, + showToday: false, +}; + +Range_Picker_Choose_Time_Hide_Buttons.args = { + ...pickerArgs, + showToday: false, + showNow: false, + showOk: false, }; Preset_Ranges.args = { ...pickerArgs, + showToday: false, }; Select_Range_By_Day_Limit.args = { ...pickerArgs, + showToday: false, }; Date_Format_Basic.args = { @@ -576,6 +649,7 @@ Date_Format_Basic.args = { Date_Format_Range.args = { ...pickerArgs, + showToday: false, }; Extra_Footer.args = { @@ -584,6 +658,7 @@ Extra_Footer.args = { Customized_Date_Styling.args = { ...pickerArgs, + showToday: false, }; Single_Borderless.args = { @@ -593,6 +668,7 @@ Single_Borderless.args = { Range_Borderless.args = { ...pickerArgs, + showToday: false, bordered: false, }; @@ -602,4 +678,5 @@ Single_Status.args = { Range_Status.args = { ...pickerArgs, + showToday: false, }; diff --git a/src/components/DateTimePicker/DatePicker/Generate/Generate.tsx b/src/components/DateTimePicker/DatePicker/Generate/Generate.tsx index 328346077..63f6bb222 100644 --- a/src/components/DateTimePicker/DatePicker/Generate/Generate.tsx +++ b/src/components/DateTimePicker/DatePicker/Generate/Generate.tsx @@ -8,6 +8,7 @@ import type { } from '../../Internal/OcPicker.types'; import generateRangePicker from './generateRangePicker'; import generateSinglePicker from './generateSinglePicker'; +import { ButtonProps } from '../../../Button'; function toArray(list: T | T[]): T[] { if (!list) { @@ -30,8 +31,11 @@ export function getTimeProps( const firstFormat: string = toArray(format)[0]; const showTimeObj: { format?: string; + nowButtonProps?: ButtonProps; + okButtonProps?: ButtonProps; picker?: OcPickerMode; showNow?: boolean; + showOk?: boolean; showHour?: boolean; showMinute?: boolean; showSecond?: boolean; diff --git a/src/components/DateTimePicker/DatePicker/Generate/generateRangePicker.tsx b/src/components/DateTimePicker/DatePicker/Generate/generateRangePicker.tsx index 2a8fa96b3..b640e9658 100644 --- a/src/components/DateTimePicker/DatePicker/Generate/generateRangePicker.tsx +++ b/src/components/DateTimePicker/DatePicker/Generate/generateRangePicker.tsx @@ -62,13 +62,20 @@ export default function generateRangePicker( getPopupContainer, id, locale = enUS, + nowButtonProps, nowText: defaultNowText, + okButtonProps, okText: defaultOkText, placeholder, popupPlacement, shape = DatePickerShape.Rectangle, + showNow = true, + showOk = true, + showToday = false, size = DatePickerSize.Medium, status, + todayButtonProps, + todayActive = false, todayText: defaultTodayText, ...rest } = props; @@ -220,8 +227,12 @@ export default function generateRangePicker( popupPlacement )} popupPlacement={popupPlacement} + nowButtonProps={nowButtonProps} nowText={nowText} + okButtonProps={okButtonProps} okText={okText} + todayButtonProps={todayButtonProps} + todayActive={todayActive} todayText={todayText} placeholder={getRangePlaceholder(picker, locale, placeholder)} suffixIcon={suffixNode} @@ -291,6 +302,9 @@ export default function generateRangePicker( components={Components} direction={htmlDir} shape={mergedShape} + showNow={showNow} + showOk={showOk} + showToday={showToday} size={mergedSize} /> ); diff --git a/src/components/DateTimePicker/DatePicker/Generate/generateSinglePicker.tsx b/src/components/DateTimePicker/DatePicker/Generate/generateSinglePicker.tsx index 8021547bf..7b50d3562 100644 --- a/src/components/DateTimePicker/DatePicker/Generate/generateSinglePicker.tsx +++ b/src/components/DateTimePicker/DatePicker/Generate/generateSinglePicker.tsx @@ -77,13 +77,20 @@ export default function generatePicker( getPopupContainer, id, locale = enUS, + nowButtonProps, nowText: defaultNowText, + okButtonProps, okText: defaultOkText, placeholder, popupPlacement, shape = DatePickerShape.Rectangle, + showNow = true, + showOk = true, + showToday = true, size = DatePickerSize.Medium, status, + todayButtonProps, + todayActive = true, todayText: defaultTodayText, ...rest } = props; @@ -101,10 +108,6 @@ export default function generatePicker( blur: () => innerRef.current?.blur(), })); - const additionalProps = { - showToday: true, - }; - let additionalOverrideProps: any = {}; if (picker) { additionalOverrideProps.picker = picker; @@ -249,15 +252,21 @@ export default function generatePicker( size={pickerSizeToIconSizeMap.get(mergedSize)} /> } + nowButtonProps={nowButtonProps} nowText={nowText} + okButtonProps={okButtonProps} okText={okText} + showNow={showNow} + showOk={showOk} + showToday={showToday} + todayButtonProps={todayButtonProps} + todayActive={todayActive} todayText={todayText} prevIcon={IconName.mdiChevronLeft} nextIcon={IconName.mdiChevronRight} superPrevIcon={IconName.mdiChevronDoubleLeft} superNextIcon={IconName.mdiChevronDoubleRight} allowClear - {...additionalProps} {...rest} {...additionalOverrideProps} locale={locale!.lang} diff --git a/src/components/DateTimePicker/Internal/Hooks/useCellClassNames.ts b/src/components/DateTimePicker/Internal/Hooks/useCellClassNames.ts index 52a7309b2..5b0a4b70f 100644 --- a/src/components/DateTimePicker/Internal/Hooks/useCellClassNames.ts +++ b/src/components/DateTimePicker/Internal/Hooks/useCellClassNames.ts @@ -13,6 +13,7 @@ export default function useCellClassNames({ isSameCell, offsetCell, today, + todayActive, value, }: { generateConfig: GenerateConfig; @@ -25,6 +26,7 @@ export default function useCellClassNames({ rangedValue?: RangeValue; hoverRangedValue?: RangeValue; today?: NullableDateType; + todayActive?: boolean; value?: NullableDateType; }) { function getClassName(currentDate: DateType) { @@ -99,7 +101,7 @@ export default function useCellClassNames({ isHoverEdgeEnd && isSameCell(nextDate, rangeStart), // Others - [styles.pickerCellToday]: isSameCell(today, currentDate), + [styles.pickerCellToday]: isSameCell(today, currentDate) && todayActive, [styles.pickerCellSelected]: isSameCell(value, currentDate), }; } diff --git a/src/components/DateTimePicker/Internal/Hooks/usePickerInput.ts b/src/components/DateTimePicker/Internal/Hooks/usePickerInput.ts index 49a99a957..1a7964486 100644 --- a/src/components/DateTimePicker/Internal/Hooks/usePickerInput.ts +++ b/src/components/DateTimePicker/Internal/Hooks/usePickerInput.ts @@ -15,6 +15,7 @@ export default function usePickerInput({ onCancel, onFocus, onBlur, + changeOnBlur, }: { open: boolean; value: string; @@ -30,6 +31,7 @@ export default function usePickerInput({ onCancel: () => void; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; + changeOnBlur?: boolean; }): [ React.DOMAttributes, { focused: boolean; typing: boolean } @@ -55,7 +57,7 @@ export default function usePickerInput({ setTyping(true); triggerOpen(true); }, - onKeyDown: (e) => { + onKeyDown: (e: React.KeyboardEvent): void => { const preventDefault = (): void => { preventDefaultRef.current = true; }; @@ -104,7 +106,7 @@ export default function usePickerInput({ } }, - onFocus: (e) => { + onFocus: (e: React.FocusEvent): void => { setTyping(true); setFocused(true); @@ -113,7 +115,7 @@ export default function usePickerInput({ } }, - onBlur: (e) => { + onBlur: (e: React.FocusEvent): void => { if (preventBlurRef.current || !isClickOutside(document.activeElement)) { preventBlurRef.current = false; return; @@ -146,17 +148,17 @@ export default function usePickerInput({ }; // check if value changed - useEffect(() => { + useEffect((): void => { valueChangedRef.current = false; }, [open]); - useEffect(() => { + useEffect((): void => { valueChangedRef.current = true; }, [value]); // Global click handler - useEffect(() => - addGlobalMouseDownEvent((e: MouseEvent) => { + useEffect((): (() => void) => + addGlobalMouseDownEvent((e: MouseEvent): void => { const target: HTMLElement = getTargetFromEvent(e); if (open) { @@ -169,7 +171,7 @@ export default function usePickerInput({ requestAnimationFrame(() => { preventBlurRef.current = false; }); - } else if (!focused || clickedOutside) { + } else if (!changeOnBlur && (!focused || clickedOutside)) { triggerOpen(false); } } diff --git a/src/components/DateTimePicker/Internal/Hooks/useRangeDisabled.ts b/src/components/DateTimePicker/Internal/Hooks/useRangeDisabled.ts index 8de9c0102..d81fdb8a4 100644 --- a/src/components/DateTimePicker/Internal/Hooks/useRangeDisabled.ts +++ b/src/components/DateTimePicker/Internal/Hooks/useRangeDisabled.ts @@ -1,8 +1,8 @@ -import React from 'react'; -import type { RangeValue, OcPickerMode, Locale } from '../OcPicker.types'; -import { getValue } from '../Utils/miscUtil'; +import React, { useCallback } from 'react'; import type { GenerateConfig } from '../Generate'; -import { isSameDate, getQuarter } from '../Utils/dateUtil'; +import type { Locale, OcPickerMode, RangeValue } from '../OcPicker.types'; +import { getQuarter, isSameDate } from '../Utils/dateUtil'; +import { getValue } from '../Utils/miscUtil'; export default function useRangeDisabled( { @@ -20,31 +20,30 @@ export default function useRangeDisabled( locale: Locale; generateConfig: GenerateConfig; }, - disabledStart: boolean, - disabledEnd: boolean + firstTimeOpen: boolean ) { const startDate: DateType = getValue(selectedValue, 0); const endDate: DateType = getValue(selectedValue, 1); - function weekFirstDate(date: DateType) { + function weekFirstDate(date: DateType): DateType { return generateConfig.locale.getWeekFirstDate(locale.locale, date); } - function monthNumber(date: DateType) { + function monthNumber(date: DateType): number { const year = generateConfig.getYear(date); const month = generateConfig.getMonth(date); return year * 100 + month; } - function quarterNumber(date: DateType) { + function quarterNumber(date: DateType): number { const year = generateConfig.getYear(date); const quarter = getQuarter(generateConfig, date); return year * 10 + quarter; } - const disabledStartDate = React.useCallback( - (date: DateType) => { - if (disabledDate && disabledDate(date)) { + const disabledStartDate = useCallback( + (date: DateType): boolean => { + if (disabled[0] || (disabledDate && disabledDate(date))) { return true; } @@ -57,7 +56,7 @@ export default function useRangeDisabled( } // Disabled part - if (disabledStart && endDate) { + if (!firstTimeOpen && endDate) { switch (picker) { case 'quarter': return quarterNumber(date) > quarterNumber(endDate); @@ -75,12 +74,12 @@ export default function useRangeDisabled( return false; }, - [disabledDate, disabled[1], endDate, disabledStart] + [disabledDate, disabled[1], endDate, firstTimeOpen] ); - const disabledEndDate = React.useCallback( - (date: DateType) => { - if (disabledDate && disabledDate(date)) { + const disabledEndDate = useCallback( + (date: DateType): boolean => { + if (disabled[1] || (disabledDate && disabledDate(date))) { return true; } @@ -93,7 +92,7 @@ export default function useRangeDisabled( } // Disabled part - if (disabledEnd && startDate) { + if (!firstTimeOpen && startDate) { switch (picker) { case 'quarter': return quarterNumber(date) < quarterNumber(startDate); @@ -111,7 +110,7 @@ export default function useRangeDisabled( return false; }, - [disabledDate, disabled[0], startDate, disabledEnd] + [disabledDate, disabled[0], startDate, firstTimeOpen] ); return [disabledStartDate, disabledEndDate]; diff --git a/src/components/DateTimePicker/Internal/Hooks/useRangeOpen.ts b/src/components/DateTimePicker/Internal/Hooks/useRangeOpen.ts new file mode 100644 index 000000000..b90e5117b --- /dev/null +++ b/src/components/DateTimePicker/Internal/Hooks/useRangeOpen.ts @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react'; +import { useMergedState } from '../../../../hooks/useMergedState'; +import { useEvent } from '../../../../hooks/useEvent'; +import { requestAnimationFrameWrapper } from '../../../../shared/utilities'; + +export type SourceType = + | 'open' + | 'blur' + | 'confirm' + | 'cancel' + | 'clear' + | 'preset'; + +export default function useRangeOpen( + defaultOpen: boolean, + open: boolean, + activePickerIndex: 0 | 1 | undefined, + changeOnBlur: boolean, + startInputRef: React.RefObject, + endInputRef: React.RefObject, + startSelectedValue: any, + endSelectedValue: any, + disabled: [boolean, boolean], + onOpenChange?: (open: boolean) => void +): [ + open: boolean, + activeIndex: 0 | 1, + firstTimeOpen: boolean, + triggerOpen: ( + open: boolean, + activeIndex: 0 | 1 | false, + source: SourceType + ) => void +] { + const [firstTimeOpen, setFirstTimeOpen] = useState(false); + + const [directionalOpen, setDirectionalOpen] = useMergedState( + defaultOpen || false, + { + value: open, + } + ); + + const [mergedOpen, setMergedOpen] = useMergedState(defaultOpen || false, { + value: open, + onChange: (nextOpen) => { + onOpenChange?.(nextOpen); + }, + }); + + const [mergedActivePickerIndex, setMergedActivePickerIndex] = useMergedState< + 0 | 1 + >(0, { + value: activePickerIndex, + }); + + const [nextActiveIndex, setNextActiveIndex] = useState<0 | 1>(null); + + useEffect((): void => { + if (mergedOpen) { + setFirstTimeOpen(true); + } + }, [mergedOpen]); + + const queryNextIndex: (index: number) => 0 | 1 = (index: number) => + index === 0 ? 1 : 0; + + const triggerOpen = useEvent( + (nextOpen: boolean, index: 0 | 1 | false, source: SourceType): void => { + if (index === false) { + // Only when `nextOpen` is false and no need open to next index + setMergedOpen(nextOpen); + } else if (nextOpen) { + setMergedActivePickerIndex(index); + setMergedOpen(nextOpen); + + const nextIndex: 0 | 1 = queryNextIndex(index); + + // Record next open index + if ( + !mergedOpen || + // Also set next index if next is empty + ![startSelectedValue, endSelectedValue][nextIndex] + ) { + setNextActiveIndex(nextIndex); + } else { + setFirstTimeOpen(false); + + if (nextActiveIndex !== null) { + setNextActiveIndex(null); + } + } + } else if (source === 'confirm' || (source === 'blur' && changeOnBlur)) { + const customNextActiveIndex = directionalOpen + ? queryNextIndex(index) + : nextActiveIndex; + + if (customNextActiveIndex !== null) { + setFirstTimeOpen(false); + setMergedActivePickerIndex(customNextActiveIndex); + } + + setNextActiveIndex(null); + + // Focus back + if ( + customNextActiveIndex !== null && + !disabled[customNextActiveIndex] + ) { + requestAnimationFrameWrapper(() => { + const ref = [startInputRef, endInputRef][customNextActiveIndex]; + ref.current?.focus(); + }); + } else { + setMergedOpen(false); + } + } else { + setMergedOpen(false); + setDirectionalOpen(false); + } + } + ); + + return [mergedOpen, mergedActivePickerIndex, firstTimeOpen, triggerOpen]; +} diff --git a/src/components/DateTimePicker/Internal/OcPicker.tsx b/src/components/DateTimePicker/Internal/OcPicker.tsx index 585111723..004edf06c 100644 --- a/src/components/DateTimePicker/Internal/OcPicker.tsx +++ b/src/components/DateTimePicker/Internal/OcPicker.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { CustomFormat, DatePickerShape, @@ -44,6 +44,7 @@ function InnerPicker(props: OcPickerProps) { autoComplete = 'off', autoFocus, bordered = true, + changeOnBlur = true, classNames, clearIcon, clearIconAriaLabelText, @@ -61,7 +62,9 @@ function InnerPicker(props: OcPickerProps) { inputReadOnly, inputRender, locale, + nowButtonProps, nowText, + okButtonProps, okText, onBlur, onChange, @@ -83,18 +86,23 @@ function InnerPicker(props: OcPickerProps) { popupPlacement, popupStyle, shape = DatePickerShape.Rectangle, + showNow, + showOk, showTime, + showToday, size = DatePickerSize.Medium, style, suffixIcon, tabIndex, + todayButtonProps, + todayActive, todayText, use12Hours, value, } = props as MergedOcPickerProps; const inputRef: React.MutableRefObject = - React.useRef(null); + useRef(null); const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time'; @@ -104,11 +112,11 @@ function InnerPicker(props: OcPickerProps) { ); const partialDivRef: React.MutableRefObject = - React.useRef(null); + useRef(null); const inputDivRef: React.MutableRefObject = - React.useRef(null); + useRef(null); const containerRef: React.MutableRefObject = - React.useRef(null); + useRef(null); // Real value const [mergedValue, setInnerValue] = useMergedState(null, { @@ -123,7 +131,7 @@ function InnerPicker(props: OcPickerProps) { // Operation ref const operationRef: React.MutableRefObject = - React.useRef(null); + useRef(null); // Open const [mergedOpen, triggerInnerOpen] = useMergedState(false, { @@ -209,6 +217,16 @@ function InnerPicker(props: OcPickerProps) { } }; + const onInternalBlur: React.FocusEventHandler = ( + e: React.FocusEvent + ): void => { + if (changeOnBlur) { + triggerChange(selectedValue); + } + + onBlur?.(e); + }; + const [inputProps, { focused, typing }] = usePickerInput({ blurToCancel: needConfirmButton, open: mergedOpen, @@ -247,11 +265,12 @@ function InnerPicker(props: OcPickerProps) { onKeyDown?.(e, preventDefault); }, onFocus, - onBlur, + onBlur: onInternalBlur, + changeOnBlur, }); // Close should sync back with text value - useEffect(() => { + useEffect((): void => { if (!mergedOpen) { setSelectedValue(mergedValue); @@ -264,14 +283,14 @@ function InnerPicker(props: OcPickerProps) { }, [mergedOpen, valueTexts]); // Change picker should sync back with text value - useEffect(() => { + useEffect((): void => { if (!mergedOpen) { resetText(); } }, [picker]); // Sync innerValue with control mode - useEffect(() => { + useEffect((): void => { // Sync select value setSelectedValue(mergedValue); }, [mergedValue]); @@ -279,14 +298,10 @@ function InnerPicker(props: OcPickerProps) { if (pickerRef) { pickerRef.current = { focus: () => { - if (inputRef.current) { - inputRef.current.focus(); - } + inputRef.current?.focus(); }, blur: () => { - if (inputRef.current) { - inputRef.current.blur(); - } + inputRef.current?.blur(); }, }; } @@ -321,8 +336,15 @@ function InnerPicker(props: OcPickerProps) { value={selectedValue} locale={locale} tabIndex={-1} + nowButtonProps={nowButtonProps} nowText={nowText} + okButtonProps={okButtonProps} okText={okText} + showNow={showNow} + showOk={showOk} + showToday={showToday} + todayButtonProps={todayButtonProps} + todayActive={todayActive} todayText={todayText} onSelect={(date: DateType) => { onSelect?.(date); @@ -355,7 +377,18 @@ function InnerPicker(props: OcPickerProps) { let suffixNode: React.ReactNode; if (suffixIcon) { - suffixNode = {suffixIcon}; + suffixNode = ( + + ): void => { + e.preventDefault(); + }} + > + {suffixIcon} + + ); } let clearNode: React.ReactNode; diff --git a/src/components/DateTimePicker/Internal/OcPicker.types.ts b/src/components/DateTimePicker/Internal/OcPicker.types.ts index 94e105870..863ab10ec 100644 --- a/src/components/DateTimePicker/Internal/OcPicker.types.ts +++ b/src/components/DateTimePicker/Internal/OcPicker.types.ts @@ -6,6 +6,7 @@ import type { AlignType } from '../../Align/Align.types'; import { IconName } from '../../Icon'; import { tuple } from '../../../shared/utilities'; import { ConfigContextProps, Shape, Size } from '../../ConfigProvider'; +import { ButtonProps } from '../../Button'; export type Locale = { /** @@ -304,6 +305,11 @@ export type PartialSharedProps = { * The custom super prev icon. */ superPrevIcon?: IconName; + /** + * The current day appears to be active. + * @default true + */ + todayActive?: boolean; /** * The partial date value. */ @@ -510,15 +516,29 @@ export type OcPickerPartialDateProps = { * Specify time that may not be selected. */ disabledTime?: DisabledTime; + /** + * The 'Now' button props. + */ + nowButtonProps?: ButtonProps; + /** + * The 'OK' button props. + */ + okButtonProps?: ButtonProps; /** * The picker type. * @default date */ picker?: 'date'; /** - * Show 'Now' button in partial when `showTime` is set. + * Show the 'Now' button in partial when `showTime` is set. + * @default true */ showNow?: boolean; + /** + * Show the 'OK' button in partial when `showTime` is set. + * @default false + */ + showOk?: boolean; /** * Enables time selection partial. */ @@ -527,6 +547,15 @@ export type OcPickerPartialDateProps = { * Show the `Today` button. */ showToday?: boolean; + /** + * The 'Today' button props. + */ + todayButtonProps?: ButtonProps; + /** + * The current day appears to be active. + * @default true + */ + todayActive?: boolean; } & OcPickerPartialSharedProps; export type OcPickerPartialTimeProps = { @@ -575,6 +604,11 @@ export type OcPickerSharedProps = { * Determines if the picker has a border style. */ bordered?: boolean; + /** + * Triggers the `onChange` event on blur. + * @default true + */ + changeOnBlur?: boolean; /** * The clear icon 'Clear' aria label text string. * @default 'Clear' @@ -656,11 +690,19 @@ export type OcPickerSharedProps = { * The custom next icon. */ nextIcon?: IconName; + /** + * The 'Now' button props. + */ + nowButtonProps?: ButtonProps; /** * The 'Now' text string. * @default 'Now' */ nowText?: string; + /** + * The 'OK' button props. + */ + okButtonProps?: ButtonProps; /** * The 'OK' text string. * @default 'OK' @@ -745,6 +787,20 @@ export type OcPickerSharedProps = { * The picker shape. */ shape?: DatePickerShape | Shape; + /** + * Show the 'Now' button in partial when `showTime` is set. + * @default true + */ + showNow?: boolean; + /** + * Show the 'OK' button + * @default true + */ + showOk?: boolean; + /** + * Show the `Today` button. + */ + showToday?: boolean; /** * The picker size. */ @@ -765,6 +821,15 @@ export type OcPickerSharedProps = { * The picker tab index. */ tabIndex?: number; + /** + * The 'Today' button props. + */ + todayButtonProps?: ButtonProps; + /** + * The current day appears to be active. + * @default true + */ + todayActive?: boolean; /** * The 'Today' text string. * @default 'Today' @@ -867,6 +932,11 @@ export type OcRangePickerSharedProps = { * @default 'off' */ autoComplete?: string; + /** + * Triggers the `onChange` event on blur. + * @default true + */ + changeOnBlur?: boolean; /** * Custom rendering function for date cells. */ @@ -900,6 +970,10 @@ export type OcRangePickerSharedProps = { * The picker partial mode. */ mode?: [PartialMode, PartialMode]; + /** + * The 'Now' button props. + */ + nowButtonProps?: ButtonProps; /** * Callback executes on range picker blur event. */ @@ -954,6 +1028,10 @@ export type OcRangePickerSharedProps = { values: RangeValue, modes: [PartialMode, PartialMode] ) => void; + /** + * The 'OK' button props. + */ + okButtonProps?: ButtonProps; partialRender?: (originPartial: React.ReactNode) => React.ReactNode; /** * The placeholder text of the range inputs. @@ -979,6 +1057,16 @@ export type OcRangePickerSharedProps = { * The range picker shape. */ shape?: DatePickerShape | Shape; + /** + * Show the 'Now' button in partial when `showTime` is set. + * @default true + */ + showNow?: boolean; + /** + * Show the 'OK' button + * @default true + */ + showOk?: boolean; /** * The range picker size. */ @@ -987,6 +1075,19 @@ export type OcRangePickerSharedProps = { * Enables time selection partial. */ showTime?: any; + /** + * Show the `Today` button. + */ + showToday?: boolean; + /** + * The 'Today' button props. + */ + todayButtonProps?: ButtonProps; + /** + * The current day appears to be active. + * @default true + */ + todayActive?: boolean; /** * The date range value. */ diff --git a/src/components/DateTimePicker/Internal/OcPickerPartial.tsx b/src/components/DateTimePicker/Internal/OcPickerPartial.tsx index 6a748b712..b5b77312a 100644 --- a/src/components/DateTimePicker/Internal/OcPickerPartial.tsx +++ b/src/components/DateTimePicker/Internal/OcPickerPartial.tsx @@ -28,7 +28,7 @@ import RangeContext from './RangeContext'; import { getExtraFooter } from './Utils/getExtraFooter'; import getRanges from './Utils/getRanges'; import { getLowerBoundTime, setDateTime, setTime } from './Utils/timeUtil'; -import { ButtonSize, SystemUIButton } from '../../Button'; +import { Button, ButtonSize, ButtonVariant } from '../../Button'; import { Breakpoints, useMatchMedia } from '../../../hooks/useMatchMedia'; import { Size } from '../../ConfigProvider'; import { DatePickerSize } from './OcPicker.types'; @@ -56,8 +56,10 @@ function OcPickerPartial(props: OcPickerPartialProps) { locale, minuteStep = 1, mode, - okText, + nowButtonProps, nowText, + okButtonProps, + okText, onChange, onMouseDown, onOk, @@ -69,11 +71,14 @@ function OcPickerPartial(props: OcPickerPartialProps) { renderExtraFooter, secondStep = 1, showNow, + showOk, showTime, showToday, size = DatePickerSize.Medium, style, tabIndex = 0, + todayButtonProps, + todayActive, todayText, value, } = props as MergedPickerPartialProps; @@ -384,6 +389,7 @@ function OcPickerPartial(props: OcPickerPartialProps) { triggerSelect(date, type); }} size={size} + todayActive={todayActive} /> ); } @@ -417,7 +423,9 @@ function OcPickerPartial(props: OcPickerPartialProps) { rangesNode = getRanges({ components, needConfirmButton, + nowButtonProps, nowText, + okButtonProps, okDisabled: !mergedValue || (disabledDate && disabledDate(mergedValue)), okText, onNow: needConfirmButton && onNow, @@ -430,6 +438,7 @@ function OcPickerPartial(props: OcPickerPartialProps) { } }, showNow, + showOk, size: size, }); } @@ -450,19 +459,23 @@ function OcPickerPartial(props: OcPickerPartialProps) { const now: DateType = generateConfig.getNow(); const disabled: boolean = disabledDate && disabledDate(now); todayNode = ( - { + onClick={(e: React.MouseEvent) => { if (!disabled) { triggerSelect(now, 'mouse', true); } + todayButtonProps?.onClick?.(e); }} size={datePickerSizeToButtonSizeMap.get(size)} - text={todayText} /> ); } diff --git a/src/components/DateTimePicker/Internal/OcRangePicker.tsx b/src/components/DateTimePicker/Internal/OcRangePicker.tsx index 022ec038a..2dfb45a6f 100644 --- a/src/components/DateTimePicker/Internal/OcRangePicker.tsx +++ b/src/components/DateTimePicker/Internal/OcRangePicker.tsx @@ -1,5 +1,8 @@ import React, { useRef, useEffect, useState } from 'react'; -import { mergeClasses } from '../../../shared/utilities'; +import { + mergeClasses, + requestAnimationFrameWrapper, +} from '../../../shared/utilities'; import { useMergedState } from '../../../hooks/useMergedState'; import type { EventValue, @@ -47,6 +50,7 @@ import type { GenerateConfig } from './Generate'; import type { OcPickerPartialProps } from './'; import RangeContext from './RangeContext'; import useRangeDisabled from './Hooks/useRangeDisabled'; +import useRangeOpen from './Hooks/useRangeOpen'; import { getExtraFooter } from './Utils/getExtraFooter'; import getRanges from './Utils/getRanges'; import useRangeViewDates from './Hooks/useRangeViewDates'; @@ -110,6 +114,7 @@ function InnerRangePicker(props: OcRangePickerProps) { autoComplete = 'off', autoFocus, bordered = true, + changeOnBlur = true, classNames, clearIconAriaLabelText, clearIcon, @@ -131,7 +136,9 @@ function InnerRangePicker(props: OcRangePickerProps) { inputReadOnly, locale, mode, + nowButtonProps, nowText, + okButtonProps, okText, onBlur, onCalendarChange, @@ -158,11 +165,16 @@ function InnerRangePicker(props: OcRangePickerProps) { renderExtraFooter, separator = ',', shape = DatePickerShape.Rectangle, + showOk = true, + showNow = true, showTime, + showToday = true, size = DatePickerSize.Medium, style, suffixIcon, use12Hours, + todayButtonProps, + todayActive, todayText, value, } = props as MergedOcRangePickerProps; @@ -170,10 +182,6 @@ function InnerRangePicker(props: OcRangePickerProps) { const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time'; - // We record opened status here in case repeat open with picker - const openRecordsRef: React.MutableRefObject> = - useRef>({}); - const containerRef: React.MutableRefObject = useRef(null); const partialDivRef: React.MutableRefObject = @@ -195,13 +203,6 @@ function InnerRangePicker(props: OcRangePickerProps) { getDefaultFormat(format, picker, showTime, use12Hours) ); - // Active picker - const [mergedActivePickerIndex, setMergedActivePickerIndex] = useMergedState< - 0 | 1 - >(0, { - value: activePickerIndex, - }); - // Operation ref const operationRef: React.MutableRefObject = useRef(null); @@ -248,6 +249,7 @@ function InnerRangePicker(props: OcRangePickerProps) { for (let i: number = 0; i < 2; i += 1) { if ( mergedDisabled[i] && + !postValues && !getValue(postValues, i) && !getValue(allowEmpty, i) ) { @@ -264,7 +266,7 @@ function InnerRangePicker(props: OcRangePickerProps) { value: mode, }); - useEffect(() => { + useEffect((): void => { setInnerModes([picker, picker]); }, [picker]); @@ -279,6 +281,23 @@ function InnerRangePicker(props: OcRangePickerProps) { } }; + const [mergedOpen, mergedActivePickerIndex, firstTimeOpen, triggerOpen] = + useRangeOpen( + defaultOpen, + open, + activePickerIndex, + changeOnBlur, + startInputRef, + endInputRef, + getValue(selectedValue, 0), + getValue(selectedValue, 1), + mergedDisabled, + onOpenChange + ); + + const startOpen: boolean = mergedOpen && mergedActivePickerIndex === 0; + const endOpen: boolean = mergedOpen && mergedActivePickerIndex === 1; + const [disabledStartDate, disabledEndDate] = useRangeDisabled( { picker, @@ -288,29 +307,9 @@ function InnerRangePicker(props: OcRangePickerProps) { disabledDate, generateConfig, }, - openRecordsRef.current[1], - openRecordsRef.current[0] + !mergedOpen || firstTimeOpen ); - const [mergedOpen, triggerInnerOpen] = useMergedState(false, { - value: open, - defaultValue: defaultOpen, - postState: (postOpen) => - mergedDisabled[mergedActivePickerIndex] ? false : postOpen, - onChange: (newOpen: boolean) => { - if (onOpenChange) { - onOpenChange(newOpen); - } - - if (!newOpen && operationRef.current && operationRef.current.onClose) { - operationRef.current.onClose(); - } - }, - }); - - const startOpen: boolean = mergedOpen && mergedActivePickerIndex === 0; - const endOpen: boolean = mergedOpen && mergedActivePickerIndex === 1; - // Popup min width const [popupMinWidth, setPopupMinWidth] = useState(0); useEffect(() => { @@ -319,42 +318,11 @@ function InnerRangePicker(props: OcRangePickerProps) { } }, [mergedOpen]); - const triggerRef: React.MutableRefObject = React.useRef(); - - function triggerOpen(newOpen: boolean, index: 0 | 1) { - if (newOpen) { - clearTimeout(triggerRef.current); - openRecordsRef.current[index] = true; - - setMergedActivePickerIndex(index); - triggerInnerOpen(newOpen); - - // Open to reset view date - if (!mergedOpen) { - setViewDate(null, index); - } - } else if (mergedActivePickerIndex === index) { - triggerInnerOpen(newOpen); - - // Clean up async - // This ensures ref doesn't quick refresh in case user opens another input with blur trigger - const openRecords = openRecordsRef.current; - triggerRef.current = setTimeout(() => { - if (openRecords === openRecordsRef.current) { - openRecordsRef.current = {}; - } - }); - } - } - function triggerOpenAndFocus(index: 0 | 1): void { - triggerOpen(true, index); - // Use setTimeout to make sure partial DOM exists - setTimeout(() => { + triggerOpen(true, index, 'open'); + requestAnimationFrameWrapper((): void => { const inputRef = [startInputRef, endInputRef][index]; - if (inputRef.current) { - inputRef.current.focus(); - } + inputRef.current?.focus(); }, 0); } @@ -393,11 +361,6 @@ function InnerRangePicker(props: OcRangePickerProps) { startValue = null; values = [null, endValue]; } - - // Clean up cache since invalidate - openRecordsRef.current = { - [sourceIndex]: true, - }; } else if (picker !== 'time' || order !== false) { // Reorder when in same date values = reorderValues(values, generateConfig); @@ -460,27 +423,6 @@ function InnerRangePicker(props: OcRangePickerProps) { onChange(values, [startStr, endStr]); } } - - // Always open another picker if possible - let nextOpenIndex: 0 | 1 = null; - if (sourceIndex === 0 && !mergedDisabled[1]) { - nextOpenIndex = 1; - } else if (sourceIndex === 1 && !mergedDisabled[0]) { - nextOpenIndex = 0; - } - - if ( - nextOpenIndex !== null && - nextOpenIndex !== mergedActivePickerIndex && - (!openRecordsRef.current[nextOpenIndex] || - !getValue(values, nextOpenIndex)) && - getValue(values, sourceIndex) - ) { - // Delay to focus to avoid input blur trigger expired selectedValues - triggerOpenAndFocus(nextOpenIndex); - } else { - triggerOpen(false, sourceIndex); - } } const forwardKeyDown = (e: React.KeyboardEvent): boolean => { @@ -489,7 +431,7 @@ function InnerRangePicker(props: OcRangePickerProps) { return operationRef.current.onKeyDown(e); } - return null; + return false; }; const sharedTextHooksProps = { @@ -578,10 +520,31 @@ function InnerRangePicker(props: OcRangePickerProps) { } }; + const [delayOpen, setDelayOpen] = useState(mergedOpen); + + useEffect((): void => { + setDelayOpen(mergedOpen); + }, [mergedOpen]); + + const onInternalBlur: React.FocusEventHandler = ( + e: React.FocusEvent + ) => { + if (changeOnBlur && delayOpen) { + const selectedIndexValue: DateType = getValue( + selectedValue, + mergedActivePickerIndex + ); + if (selectedIndexValue) { + triggerChange(selectedValue, mergedActivePickerIndex); + } + } + return onBlur?.(e); + }; + const getSharedInputHookProps = (index: 0 | 1, resetText: () => void) => ({ - blurToCancel: needConfirmButton, + blurToCancel: !changeOnBlur && needConfirmButton, forwardKeyDown, - onBlur, + onBlur: onInternalBlur, isClickOutside: (target: EventTarget | null): boolean => !elementsContains( [ @@ -593,13 +556,21 @@ function InnerRangePicker(props: OcRangePickerProps) { target as HTMLElement ), onFocus: (e: React.FocusEvent): void => { - setMergedActivePickerIndex(index); if (onFocus) { onFocus(e); } }, triggerOpen: (newOpen: boolean): void => { - triggerOpen(newOpen, index); + if (newOpen) { + triggerOpen(newOpen, index, 'open'); + } else { + triggerOpen( + newOpen, + // Close directly if no selected value provided + getValue(selectedValue, index) ? index : false, + 'blur' + ); + } }, onSubmit: (): boolean => { if ( @@ -614,23 +585,33 @@ function InnerRangePicker(props: OcRangePickerProps) { triggerChange(selectedValue, index); resetText(); - return null; + // Switch + triggerOpen(false, mergedActivePickerIndex, 'confirm'); + return true; }, onCancel: (): void => { - triggerOpen(false, index); + triggerOpen(false, index, 'cancel'); setSelectedValue(mergedValue); resetText(); }, }); + const sharedPickerInput = { + onKeyDown: ( + e: React.KeyboardEvent, + preventDefault: () => void + ): void => { + onKeyDown?.(e, preventDefault); + }, + changeOnBlur, + }; + const [startInputProps, { focused: startFocused, typing: startTyping }] = usePickerInput({ ...getSharedInputHookProps(0, resetStartText), open: startOpen, value: startText, - onKeyDown: (e, preventDefault) => { - onKeyDown?.(e, preventDefault); - }, + ...sharedPickerInput, }); const [endInputProps, { focused: endFocused, typing: endTyping }] = @@ -638,12 +619,10 @@ function InnerRangePicker(props: OcRangePickerProps) { ...getSharedInputHookProps(1, resetEndText), open: endOpen, value: endText, - onKeyDown: (e, preventDefault) => { - onKeyDown?.(e, preventDefault); - }, + ...sharedPickerInput, }); - const onPickerClick = (e: React.MouseEvent) => { + const onPickerClick = (e: React.MouseEvent): void => { // When click inside the picker & outside the picker's input elements // the partial should still be opened if (onClick) { @@ -719,18 +698,12 @@ function InnerRangePicker(props: OcRangePickerProps) { if (pickerRef) { pickerRef.current = { - focus: () => { - if (startInputRef.current) { - startInputRef.current.focus(); - } + focus: (): void => { + startInputRef?.current.focus(); }, - blur: () => { - if (startInputRef.current) { - startInputRef.current.blur(); - } - if (endInputRef.current) { - endInputRef.current.blur(); - } + blur: (): void => { + startInputRef?.current.blur(); + endInputRef?.current.blur(); }, }; } @@ -752,7 +725,7 @@ function InnerRangePicker(props: OcRangePickerProps) { label, onClick: () => { triggerChange(newValues, null); - triggerOpen(false, mergedActivePickerIndex); + triggerOpen(false, mergedActivePickerIndex, 'preset'); }, onMouseEnter: () => { setRangeHoverValue(newValues); @@ -810,7 +783,14 @@ function InnerRangePicker(props: OcRangePickerProps) { {...(props as any)} {...partialProps} dateRender={partialDateRender} + nowButtonProps={nowButtonProps} + okButtonProps={okButtonProps} + showOk={showOk} + showNow={showNow} showTime={partialShowTime} + showToday={showToday} + todayButtonProps={todayButtonProps} + todayActive={todayActive} mode={mergedModes[mergedActivePickerIndex]} generateConfig={generateConfig} style={undefined} @@ -886,7 +866,8 @@ function InnerRangePicker(props: OcRangePickerProps) { mergedActivePickerIndex && startInputDivRef.current && separatorRef.current && - partialDivRef.current + partialDivRef.current && + arrowRef.current ) { // Arrow offset arrowLeft = @@ -925,18 +906,26 @@ function InnerRangePicker(props: OcRangePickerProps) { okDisabled: !getValue(selectedValue, mergedActivePickerIndex) || (disabledDate && disabledDate(selectedValue[mergedActivePickerIndex])), + nowButtonProps, nowText, + okButtonProps, okText, onOk: () => { - if (getValue(selectedValue, mergedActivePickerIndex)) { - // triggerChangeOld(selectedValue); + const selectedIndexValue = getValue( + selectedValue, + mergedActivePickerIndex + ); + if (selectedIndexValue) { triggerChange(selectedValue, mergedActivePickerIndex); - if (onOk) { - onOk(selectedValue); - } + onOk?.(selectedValue); + + // Switch + triggerOpen(false, mergedActivePickerIndex, 'confirm'); } }, rangeList, + showNow, + showOk, size: size, }); @@ -1047,8 +1036,8 @@ function InnerRangePicker(props: OcRangePickerProps) { let clearNode: React.ReactNode; if ( allowClear && - ((getValue(mergedValue, 0) && !mergedDisabled[0]) || - (getValue(mergedValue, 1) && !mergedDisabled[1])) + ((getValue(mergedValue as RangeValue, 0) && !mergedDisabled[0]) || + (getValue(mergedValue as RangeValue, 1) && !mergedDisabled[1])) ) { clearNode = ( (props: OcRangePickerProps) { } triggerChange(values, null); - triggerOpen(false, mergedActivePickerIndex); + triggerOpen(false, mergedActivePickerIndex, 'clear'); }} className={styles.pickerClear} role="button" @@ -1131,6 +1120,14 @@ function InnerRangePicker(props: OcRangePickerProps) { } else { onEndLeave(); } + + // Switch + const nextActivePickerIndex = mergedActivePickerIndex === 0 ? 1 : 0; + if (mergedDisabled[nextActivePickerIndex]) { + triggerOpen(false, false, 'confirm'); + } else { + triggerOpen(false, mergedActivePickerIndex, 'confirm'); + } } else { setSelectedValue(values); } diff --git a/src/components/DateTimePicker/Internal/Partials/DatePartial/Date.types.ts b/src/components/DateTimePicker/Internal/Partials/DatePartial/Date.types.ts index e90949d04..09cea17c7 100644 --- a/src/components/DateTimePicker/Internal/Partials/DatePartial/Date.types.ts +++ b/src/components/DateTimePicker/Internal/Partials/DatePartial/Date.types.ts @@ -132,6 +132,11 @@ export type DateBodyProps = { * @default DatePickerSize.Medium */ size?: DatePickerSize | Size; + /** + * The current day appears to be active. + * @default true + */ + todayActive?: boolean; /** * The current date value. */ diff --git a/src/components/DateTimePicker/Internal/Partials/DatePartial/DateBody.tsx b/src/components/DateTimePicker/Internal/Partials/DatePartial/DateBody.tsx index aa5042044..eb1ed9149 100644 --- a/src/components/DateTimePicker/Internal/Partials/DatePartial/DateBody.tsx +++ b/src/components/DateTimePicker/Internal/Partials/DatePartial/DateBody.tsx @@ -19,6 +19,7 @@ function DateBody(props: DateBodyProps) { locale, rowCount, size = DatePickerSize.Medium, + todayActive, value, viewDate, } = props; @@ -50,6 +51,7 @@ function DateBody(props: DateBodyProps) { const getCellClassNames = useCellClassNames({ today, + todayActive, value, generateConfig, rangedValue: rangedValue, diff --git a/src/components/DateTimePicker/Internal/Partials/DatetimePartial/Datetime.tsx b/src/components/DateTimePicker/Internal/Partials/DatetimePartial/Datetime.tsx index 088459cfc..a471f24b4 100644 --- a/src/components/DateTimePicker/Internal/Partials/DatetimePartial/Datetime.tsx +++ b/src/components/DateTimePicker/Internal/Partials/DatetimePartial/Datetime.tsx @@ -13,6 +13,7 @@ import type { } from '../../OcPicker.types'; import { DatePickerSize } from '../../OcPicker.types'; import { Shape, Size } from '../../../../ConfigProvider'; +import { ButtonProps } from '../../../../Button'; import styles from '../../ocpicker.module.scss'; @@ -40,7 +41,10 @@ function DatetimePartial(props: DatetimePartialProps) { const timeProps: { format?: string; + nowButtonProps?: ButtonProps; + okButtonProps?: ButtonProps; showNow?: boolean; + showOk?: boolean; showHour?: boolean; showMinute?: boolean; showSecond?: boolean; diff --git a/src/components/DateTimePicker/Internal/Partials/TimePartial/Time.types.ts b/src/components/DateTimePicker/Internal/Partials/TimePartial/Time.types.ts index ffcf5fccb..7e6b5b60d 100644 --- a/src/components/DateTimePicker/Internal/Partials/TimePartial/Time.types.ts +++ b/src/components/DateTimePicker/Internal/Partials/TimePartial/Time.types.ts @@ -8,6 +8,7 @@ import type { } from '../../OcPicker.types'; import type { GenerateConfig } from '../../Generate'; import { Shape, Size } from '../../../../ConfigProvider'; +import { ButtonProps } from '../../../../Button'; export type Unit = { /** @@ -83,6 +84,14 @@ export type SharedTimeProps = { * @default 1 */ minuteStep?: number; + /** + * The 'Now' button props. + */ + nowButtonProps?: ButtonProps; + /** + * The 'OK' button props. + */ + okButtonProps?: ButtonProps; /** * The second step * @default 1 @@ -104,6 +113,10 @@ export type SharedTimeProps = { * Show 'Now' button in partial when `showTime` is set. */ showNow?: boolean; + /** + * Show 'OK' button in partial when `showTime` is set. + */ + showOk?: boolean; /** * Whether to show the second column. */ diff --git a/src/components/DateTimePicker/Internal/Tests/blur.test.tsx b/src/components/DateTimePicker/Internal/Tests/blur.test.tsx new file mode 100644 index 000000000..3efeb3b9a --- /dev/null +++ b/src/components/DateTimePicker/Internal/Tests/blur.test.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import Enzyme from 'enzyme'; +import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; +import MockDate from 'mockdate'; +import { + getDayjs, + isOpen, + DayjsPicker, + DayjsRangePicker, + openPicker, +} from './util/commonUtil'; +import { fireEvent, render } from '@testing-library/react'; + +Enzyme.configure({ adapter: new Adapter() }); + +describe('Picker.changeOnBlur', () => { + beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }); + + beforeEach(() => { + MockDate.set(getDayjs('1990-09-03 00:00:00').toDate()); + }); + + afterEach(() => { + MockDate.reset(); + }); + + test('Picker', () => { + const onSelect = jest.fn(); + const onChange = jest.fn(); + + const { container } = render( + <> + +