From bc7b09e5ea6693e7cc0c5ccf7546bbdcbb7bc4a0 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 10 Apr 2024 23:27:08 +1000 Subject: [PATCH] Fix initial focus selection --- .../src/NumberField/NumberFieldInput.test.tsx | 30 +++++++++++++++++++ .../src/useNumberField/useNumberField.ts | 26 ++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/mui-base/src/NumberField/NumberFieldInput.test.tsx b/packages/mui-base/src/NumberField/NumberFieldInput.test.tsx index 4fc4c72946..adc18e5f9b 100644 --- a/packages/mui-base/src/NumberField/NumberFieldInput.test.tsx +++ b/packages/mui-base/src/NumberField/NumberFieldInput.test.tsx @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { act, createRenderer, screen } from '@mui/internal-test-utils'; import { NumberField } from '@base_ui/react/NumberField'; import { fireEvent } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; import { describeConformance } from '../../test/describeConformance'; import { NumberFieldContext, NumberFieldContextValue } from './NumberFieldContext'; @@ -193,4 +194,33 @@ describe('', () => { fireEvent.blur(input); expect(input).to.have.value('4'); }); + + it('should set selection to the logical end of the input on first focus', async () => { + if (/jsdom/.test(window.navigator.userAgent)) { + // JSDOM does not support selectionStart/selectionEnd + return; + } + + render( + + + + , + ); + const input = screen.getByRole('textbox'); + const label = screen.getByLabelText('label'); + + await userEvent.click(label); + + expect(input.selectionStart).to.equal(3); + expect(input.selectionEnd).to.equal(3); + + input.setSelectionRange(0, 3); + + await userEvent.click(document.body); + await userEvent.click(label); + + expect(input.selectionStart).to.equal(0); + expect(input.selectionEnd).to.equal(3); + }); }); diff --git a/packages/mui-base/src/useNumberField/useNumberField.ts b/packages/mui-base/src/useNumberField/useNumberField.ts index 061b1ab1dd..7533185a38 100644 --- a/packages/mui-base/src/useNumberField/useNumberField.ts +++ b/packages/mui-base/src/useNumberField/useNumberField.ts @@ -84,7 +84,8 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa const movesAfterTouchRef = React.useRef(0); const allowInputSyncRef = React.useRef(true); const unsubscribeFromGlobalContextMenuRef = React.useRef<() => void>(() => {}); - const isTouchingRef = React.useRef(false); + const isTouchingButtonRef = React.useRef(false); + const hasTouchedInputRef = React.useRef(false); const [valueUnwrapped, setValueUnwrapped] = useControlled({ controlled: externalValue, @@ -361,10 +362,10 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa userSelect: 'none', }, onTouchStart() { - isTouchingRef.current = true; + isTouchingButtonRef.current = true; }, onTouchEnd() { - isTouchingRef.current = false; + isTouchingButtonRef.current = false; }, onClick(event) { const isDisabled = disabled || readOnly || (isIncrement ? isMax : isMin); @@ -434,7 +435,7 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa event.defaultPrevented || isDisabled || !isPressedRef.current || - isTouchingRef.current + isTouchingButtonRef.current ) { return; } @@ -442,14 +443,14 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa startAutoChange(isIncrement); }, onMouseLeave() { - if (isTouchingRef.current) { + if (isTouchingButtonRef.current) { return; } stopAutoChange(); }, onMouseUp() { - if (isTouchingRef.current) { + if (isTouchingButtonRef.current) { return; } @@ -498,6 +499,19 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa spellCheck: 'false', 'aria-roledescription': 'Number field', 'aria-invalid': invalid || undefined, + onFocus(event) { + if (event.defaultPrevented || readOnly || disabled || hasTouchedInputRef.current) { + return; + } + + hasTouchedInputRef.current = true; + + // Browsers set selection at the start of the input field by default. We want to set it at + // the end for the first focus. + const target = event.currentTarget; + const length = target.value.length; + target.setSelectionRange(length, length); + }, onBlur(event) { if (event.defaultPrevented || readOnly || disabled) { return;