diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx index 8a3d09b6ce3742..e51e327f997376 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/TimePicker.test.tsx @@ -42,11 +42,11 @@ describe('TimePicker', () => { ); handleTimeSelect.mockClear(); - // Do not call onTimeSelect when Tab out but the value remains the same + // Do not call onTimeSelect on Enter when the value remains the same fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); expect(handleTimeSelect).toHaveBeenCalledTimes(0); - // Call onTimeSelect when Tab out and the value changes + // Call onTimeSelect on Enter when the value changes userEvent.type(input, '111{enter}'); expect(handleTimeSelect).toHaveBeenCalledTimes(1); expect(handleTimeSelect).toHaveBeenCalledWith( @@ -54,4 +54,26 @@ describe('TimePicker', () => { expect.objectContaining({ selectedTimeText: '10:30111', error: 'invalid-input' }), ); }); + + it('when freeform, trigger onTimeSelect on blur when value change', () => { + const handleTimeSelect = jest.fn(); + const { getByRole } = render( + , + ); + + const input = getByRole('combobox'); + const expandIcon = getByRole('button'); + + // Do not call onTimeSelect when clicking dropdown icon + userEvent.type(input, '111'); + userEvent.click(expandIcon); + expect(handleTimeSelect).toHaveBeenCalledTimes(0); + + // Call onTimeSelect on focus lose + userEvent.tab(); + expect(handleTimeSelect).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ selectedTimeText: '111' }), + ); + }); }); diff --git a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx index e8c66ed9371837..d95439af6d1df0 100644 --- a/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx +++ b/packages/react-components/react-timepicker-compat-preview/src/components/TimePicker/useTimePicker.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { mergeCallbacks, useControllableState } from '@fluentui/react-utilities'; +import { elementContains, mergeCallbacks, useControllableState, useMergedRefs } from '@fluentui/react-utilities'; import { Enter } from '@fluentui/keyboard-keys'; import type { Hour, TimePickerOption, TimePickerProps, TimePickerState, TimeSelectionData } from './TimePicker.types'; import { ComboboxProps, useCombobox_unstable, Option } from '@fluentui/react-combobox'; @@ -92,7 +92,6 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref { const { activeOption, freeform, validateFreeFormTime, options, submittedText, setActiveOption, value } = state; @@ -202,5 +201,25 @@ const useSelectTimeFromValue = (state: TimePickerState, callback: TimePickerProp ); state.root.onKeyDown = mergeCallbacks(handleKeyDown, state.root.onKeyDown); - // TODO call selectTimeFromValue on blur + const rootRef = React.useRef(null); + state.root.ref = useMergedRefs(state.root.ref, rootRef); + + if (state.listbox) { + state.listbox.tabIndex = -1; // allows it to be the relatedTarget of a blur event. + } + + if (state.expandIcon) { + state.expandIcon.tabIndex = -1; // allows it to be the relatedTarget of a blur event. + } + + const handleInputBlur = React.useCallback( + (e: React.FocusEvent) => { + const isOutside = e.relatedTarget ? !elementContains(rootRef.current, e.relatedTarget) : true; + if (isOutside) { + selectTimeFromValue(e); + } + }, + [selectTimeFromValue], + ); + state.input.onBlur = mergeCallbacks(handleInputBlur, state.input.onBlur); };