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);
};