From fd23a1bf61327e240963541f133845b7e083d14b Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Thu, 19 Oct 2023 17:25:42 +0200 Subject: [PATCH 1/2] blur --- .../components/TimePicker/TimePicker.test.tsx | 26 +++++++++++++-- .../components/TimePicker/useTimePicker.tsx | 32 ++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) 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 8a3d09b6ce374..e51e327f99737 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 e8c66ed937183..64cc61c946f42 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 { 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,30 @@ 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); + + const listboxRef = React.useRef(null); + const mergedListboxRef = useMergedRefs(state.listbox?.ref, listboxRef); + if (state.listbox) { + state.listbox.ref = mergedListboxRef; + 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 + ? [rootRef, listboxRef].every(({ current }) => !current?.contains(e.relatedTarget as HTMLElement)) + : true; + if (isOutside) { + selectTimeFromValue(e); + } + }, + [selectTimeFromValue], + ); + state.input.onBlur = mergeCallbacks(handleInputBlur, state.input.onBlur); }; From c2f77c1e454d90138bfa98bcaf290b8e1b32b9b5 Mon Sep 17 00:00:00 2001 From: YuanboXue-Amber Date: Tue, 24 Oct 2023 14:03:54 +0200 Subject: [PATCH 2/2] use element contains --- .../src/components/TimePicker/useTimePicker.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 64cc61c946f42..d95439af6d1df 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, useMergedRefs } 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'; @@ -204,10 +204,7 @@ const useSelectTimeFromValue = (state: TimePickerState, callback: TimePickerProp const rootRef = React.useRef(null); state.root.ref = useMergedRefs(state.root.ref, rootRef); - const listboxRef = React.useRef(null); - const mergedListboxRef = useMergedRefs(state.listbox?.ref, listboxRef); if (state.listbox) { - state.listbox.ref = mergedListboxRef; state.listbox.tabIndex = -1; // allows it to be the relatedTarget of a blur event. } @@ -217,9 +214,7 @@ const useSelectTimeFromValue = (state: TimePickerState, callback: TimePickerProp const handleInputBlur = React.useCallback( (e: React.FocusEvent) => { - const isOutside = e.relatedTarget - ? [rootRef, listboxRef].every(({ current }) => !current?.contains(e.relatedTarget as HTMLElement)) - : true; + const isOutside = e.relatedTarget ? !elementContains(rootRef.current, e.relatedTarget) : true; if (isOutside) { selectTimeFromValue(e); }