Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,38 @@ 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(
expect.anything(),
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(
<TimePicker freeform dateAnchor={dateAnchor} onTimeSelect={handleTimeSelect} startHour={10} />,
);

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' }),
);
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -92,7 +92,6 @@ export const useTimePicker_unstable = (props: TimePickerProps, ref: React.Ref<HT
// Combobox clears selection when input value not matching any option; but we allow this case in freeform TimePicker.
return;
}

const timeSelectionData: TimeSelectionData = {
selectedTime: keyToDate(data.optionValue),
selectedTimeText: data.optionText,
Expand Down Expand Up @@ -161,7 +160,7 @@ const useStableDateAnchor = (providedDate: Date | undefined, startHour: Hour, en
* Mimics the behavior of the browser's change event for a freeform TimePicker.
* The provided callback is called when input changed and:
* - Enter/Tab key is pressed on the input.
* - TODO: TimePicker loses focus, signifying a possible change.
* - TimePicker loses focus, signifying a possible change.
*/
const useSelectTimeFromValue = (state: TimePickerState, callback: TimePickerProps['onTimeSelect']) => {
const { activeOption, freeform, validateFreeFormTime, options, submittedText, setActiveOption, value } = state;
Expand Down Expand Up @@ -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<HTMLDivElement>(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<HTMLInputElement>) => {
const isOutside = e.relatedTarget ? !elementContains(rootRef.current, e.relatedTarget) : true;
if (isOutside) {
selectTimeFromValue(e);
}
},
[selectTimeFromValue],
);
state.input.onBlur = mergeCallbacks(handleInputBlur, state.input.onBlur);
};