diff --git a/packages/mui-base/src/NumberField/NumberField.types.ts b/packages/mui-base/src/NumberField/NumberField.types.ts index 98f334177c..922d3d785b 100644 --- a/packages/mui-base/src/NumberField/NumberField.types.ts +++ b/packages/mui-base/src/NumberField/NumberField.types.ts @@ -1,4 +1,4 @@ -import type { BaseUiComponentCommonProps } from '../utils/BaseUiComponentCommonProps'; +import type { BaseUIComponentProps } from '../utils/BaseUI.types'; export interface NumberFieldOwnerState { /** @@ -32,7 +32,7 @@ export interface NumberFieldOwnerState { } export interface NumberFieldProps - extends Omit, 'onChange'> { + extends Omit, 'onChange'> { /** * The id of the input element. */ @@ -116,20 +116,19 @@ export interface NumberFieldProps onChange?: (value: number | null) => void; } -export interface NumberFieldGroupProps - extends BaseUiComponentCommonProps<'div', NumberFieldOwnerState> {} +export interface NumberFieldGroupProps extends BaseUIComponentProps<'div', NumberFieldOwnerState> {} export interface NumberFieldInputProps - extends BaseUiComponentCommonProps<'input', NumberFieldOwnerState> {} + extends BaseUIComponentProps<'input', NumberFieldOwnerState> {} export interface NumberFieldIncrementProps - extends BaseUiComponentCommonProps<'button', NumberFieldOwnerState> {} + extends BaseUIComponentProps<'button', NumberFieldOwnerState> {} export interface NumberFieldDecrementProps - extends BaseUiComponentCommonProps<'button', NumberFieldOwnerState> {} + extends BaseUIComponentProps<'button', NumberFieldOwnerState> {} export interface NumberFieldScrubAreaProps - extends BaseUiComponentCommonProps<'span', NumberFieldOwnerState> { + extends BaseUIComponentProps<'span', NumberFieldOwnerState> { /** * The direction that the scrub area should change the value. * @default 'vertical' @@ -149,4 +148,4 @@ export interface NumberFieldScrubAreaProps } export interface NumberFieldScrubAreaCursorProps - extends BaseUiComponentCommonProps<'span', NumberFieldOwnerState> {} + extends BaseUIComponentProps<'span', NumberFieldOwnerState> {} diff --git a/packages/mui-base/src/useNumberField/useNumberField.ts b/packages/mui-base/src/useNumberField/useNumberField.ts index 2344e43197..dfae4de706 100644 --- a/packages/mui-base/src/useNumberField/useNumberField.ts +++ b/packages/mui-base/src/useNumberField/useNumberField.ts @@ -3,7 +3,6 @@ import { useEventCallback } from '../utils/useEventCallback'; import { useControlled } from '../utils/useControlled'; import type { NumberFieldProps } from '../NumberField'; import { useLatestRef } from '../utils/useLatestRef'; -import { defineProps } from '../utils/defineProps'; import type { UseNumberFieldReturnValue } from './useNumberField.types'; import { ownerDocument, ownerWindow } from '../utils/owner'; import { useId } from '../utils/useId'; @@ -28,6 +27,7 @@ import { START_AUTO_CHANGE_DELAY, TOUCH_TIMEOUT, } from './constants'; +import { mergeReactProps } from '../utils/mergeReactProps'; /** * The basic building block for creating custom number fields. @@ -337,16 +337,16 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa ); const getGroupProps: UseNumberFieldReturnValue['getGroupProps'] = React.useCallback( - (externalProps = {}) => ({ - role: 'group', - ...externalProps, - }), + (externalProps = {}) => + mergeReactProps(externalProps, { + role: 'group', + }), [], ); const getCommonButtonProps = React.useCallback( - (isIncrement: boolean, externalProps: React.ComponentPropsWithRef<'button'> = {}) => - defineProps<'button'>({ + (isIncrement: boolean, externalProps = {}) => + mergeReactProps<'button'>(externalProps, { disabled: disabled || (isIncrement ? isMax : isMin), type: 'button', 'aria-readonly': readOnly || undefined, @@ -356,23 +356,18 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa // to change the value. On the other hand, `aria-hidden` is not applied because touch screen // readers should be able to use the buttons. tabIndex: -1, - ...externalProps, style: { WebkitUserSelect: 'none', userSelect: 'none', - ...externalProps.style, }, - onTouchStart(event) { - externalProps.onTouchStart?.(event); + onTouchStart() { isTouchingRef.current = true; }, - onTouchEnd(event) { - externalProps.onTouchEnd?.(event); + onTouchEnd() { isTouchingRef.current = false; }, onClick(event) { const isDisabled = disabled || readOnly || (isIncrement ? isMax : isMin); - externalProps.onClick?.(event); if ( event.defaultPrevented || isDisabled || @@ -387,10 +382,8 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa incrementValue(amount, isIncrement ? 1 : -1); }, onPointerDown(event) { - externalProps.onPointerDown?.(event); const isMainButton = !event.button || event.button === 0; const isDisabled = disabled || (isIncrement ? isMax : isMin); - if (event.defaultPrevented || readOnly || !isMainButton || isDisabled) { return; } @@ -418,7 +411,6 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa } }, onPointerMove(event) { - externalProps.onPointerMove?.(event); const isDisabled = disabled || readOnly || (isIncrement ? isMax : isMin); if (isDisabled || event.pointerType !== 'touch' || !isPressedRef.current) { return; @@ -437,7 +429,6 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa } }, onMouseEnter(event) { - externalProps.onMouseEnter?.(event); const isDisabled = disabled || readOnly || (isIncrement ? isMax : isMin); if ( event.defaultPrevented || @@ -450,16 +441,14 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa startAutoChange(isIncrement); }, - onMouseLeave(event) { - externalProps.onMouseLeave?.(event); + onMouseLeave() { if (isTouchingRef.current) { return; } stopAutoChange(); }, - onMouseUp(event) { - externalProps.onMouseUp?.(event); + onMouseUp() { if (isTouchingRef.current) { return; } @@ -493,178 +482,175 @@ export function useNumberField(params: NumberFieldProps): UseNumberFieldReturnVa ); const getInputProps: UseNumberFieldReturnValue['getInputProps'] = React.useCallback( - (externalProps = {}) => ({ - id, - required, - autoFocus, - name, - disabled, - readOnly, - inputMode, - type: 'text', - autoComplete: 'off', - autoCorrect: 'off', - spellCheck: 'false', - 'aria-roledescription': 'Number field', - 'aria-invalid': invalid || undefined, - ...externalProps, - ref: inputRef, - onBlur(event) { - externalProps.onBlur?.(event); - if (event.defaultPrevented || readOnly || disabled) { - return; - } + (externalProps = {}) => + mergeReactProps<'input'>(externalProps, { + id, + required, + autoFocus, + name, + disabled, + readOnly, + inputMode, + ref: inputRef, + type: 'text', + autoComplete: 'off', + autoCorrect: 'off', + spellCheck: 'false', + 'aria-roledescription': 'Number field', + 'aria-invalid': invalid || undefined, + onBlur(event) { + if (event.defaultPrevented || readOnly || disabled) { + return; + } - allowInputSyncRef.current = true; + allowInputSyncRef.current = true; - if (inputValue.trim() === '') { - setValue(null); - return; - } + if (inputValue.trim() === '') { + setValue(null); + return; + } - const parsedValue = parseNumber(inputValue, formatOptionsRef.current); + const parsedValue = parseNumber(inputValue, formatOptionsRef.current); - if (parsedValue !== null) { - setValue(parsedValue); - } - }, - onChange(event) { - externalProps.onChange?.(event); - // Workaround for https://github.com/facebook/react/issues/9023 - if (event.nativeEvent.defaultPrevented) { - return; - } - - allowInputSyncRef.current = false; - const targetValue = event.target.value; + if (parsedValue !== null) { + setValue(parsedValue); + } + }, + onChange(event) { + // Workaround for https://github.com/facebook/react/issues/9023 + if (event.nativeEvent.defaultPrevented) { + return; + } - if (targetValue.trim() === '') { - setInputValue(targetValue); - setValue(null); - return; - } + allowInputSyncRef.current = false; + const targetValue = event.target.value; - if (event.isTrusted) { - setInputValue(targetValue); - return; - } + if (targetValue.trim() === '') { + setInputValue(targetValue); + setValue(null); + return; + } - const parsedValue = parseNumber(targetValue, formatOptionsRef.current); + if (event.isTrusted) { + setInputValue(targetValue); + return; + } - if (parsedValue !== null) { - setInputValue(targetValue); - setValue(parsedValue); - } - }, - onKeyDown(event) { - externalProps.onKeyDown?.(event); - if (event.defaultPrevented || readOnly || disabled) { - return; - } + const parsedValue = parseNumber(targetValue, formatOptionsRef.current); - allowInputSyncRef.current = true; + if (parsedValue !== null) { + setInputValue(targetValue); + setValue(parsedValue); + } + }, + onKeyDown(event) { + if (event.defaultPrevented || readOnly || disabled) { + return; + } - const allowedNonNumericKeys = getAllowedNonNumericKeys(); + allowInputSyncRef.current = true; - let isAllowedNonNumericKey = allowedNonNumericKeys.includes(event.key); + const allowedNonNumericKeys = getAllowedNonNumericKeys(); - const { decimal, currency } = getNumberLocaleDetails([], formatOptionsRef.current); + let isAllowedNonNumericKey = allowedNonNumericKeys.includes(event.key); - const selectionStart = event.currentTarget.selectionStart; - const selectionEnd = event.currentTarget.selectionEnd; - const isAllSelected = selectionStart === 0 && selectionEnd === inputValue.length; + const { decimal, currency } = getNumberLocaleDetails([], formatOptionsRef.current); - // Allow the minus key only if there isn't already a plus or minus sign, or if all the text - // is selected, or if only the minus sign is highlighted. - if (event.key === '-' && allowedNonNumericKeys.includes('-')) { - const isMinusHighlighted = - selectionStart === 0 && selectionEnd === 1 && inputValue[0] === '-'; - isAllowedNonNumericKey = !inputValue.includes('-') || isAllSelected || isMinusHighlighted; - } + const selectionStart = event.currentTarget.selectionStart; + const selectionEnd = event.currentTarget.selectionEnd; + const isAllSelected = selectionStart === 0 && selectionEnd === inputValue.length; - // Allow only one decimal separator, or if all the text is selected, or if only the decimal - // separator is highlighted. - if (event.key === decimal) { - const decimalIndex = inputValue.indexOf(decimal); - const isDecimalHighlighted = - selectionStart === decimalIndex && selectionEnd === decimalIndex + 1; - isAllowedNonNumericKey = - !inputValue.includes(decimal) || isAllSelected || isDecimalHighlighted; - } + // Allow the minus key only if there isn't already a plus or minus sign, or if all the text + // is selected, or if only the minus sign is highlighted. + if (event.key === '-' && allowedNonNumericKeys.includes('-')) { + const isMinusHighlighted = + selectionStart === 0 && selectionEnd === 1 && inputValue[0] === '-'; + isAllowedNonNumericKey = + !inputValue.includes('-') || isAllSelected || isMinusHighlighted; + } - if (event.key === currency) { - const currencyIndex = inputValue.indexOf(currency); - const isCurrencyHighlighted = - selectionStart === currencyIndex && selectionEnd === currencyIndex + 1; - isAllowedNonNumericKey = - !inputValue.includes(currency) || isAllSelected || isCurrencyHighlighted; - } + // Allow only one decimal separator, or if all the text is selected, or if only the decimal + // separator is highlighted. + if (event.key === decimal) { + const decimalIndex = inputValue.indexOf(decimal); + const isDecimalHighlighted = + selectionStart === decimalIndex && selectionEnd === decimalIndex + 1; + isAllowedNonNumericKey = + !inputValue.includes(decimal) || isAllSelected || isDecimalHighlighted; + } - const isLatinNumeral = /^[0-9]$/.test(event.key); - const isArabicNumeral = ARABIC_RE.test(event.key); - const isHanNumeral = HAN_RE.test(event.key); - const isNavigateKey = [ - 'Backspace', - 'Delete', - 'ArrowLeft', - 'ArrowRight', - 'Tab', - 'Enter', - ].includes(event.key); + if (event.key === currency) { + const currencyIndex = inputValue.indexOf(currency); + const isCurrencyHighlighted = + selectionStart === currencyIndex && selectionEnd === currencyIndex + 1; + isAllowedNonNumericKey = + !inputValue.includes(currency) || isAllSelected || isCurrencyHighlighted; + } - if ( - // Allow composition events (e.g., pinyin) - event.nativeEvent.isComposing || - event.altKey || - event.ctrlKey || - event.metaKey || - isAllowedNonNumericKey || - isLatinNumeral || - isArabicNumeral || - isHanNumeral || - isNavigateKey - ) { - return; - } + const isLatinNumeral = /^[0-9]$/.test(event.key); + const isArabicNumeral = ARABIC_RE.test(event.key); + const isHanNumeral = HAN_RE.test(event.key); + const isNavigateKey = [ + 'Backspace', + 'Delete', + 'ArrowLeft', + 'ArrowRight', + 'Tab', + 'Enter', + ].includes(event.key); - // We need to commit the number at this point if the input hasn't been blurred. - const parsedValue = parseNumber(inputValue, formatOptionsRef.current); + if ( + // Allow composition events (e.g., pinyin) + event.nativeEvent.isComposing || + event.altKey || + event.ctrlKey || + event.metaKey || + isAllowedNonNumericKey || + isLatinNumeral || + isArabicNumeral || + isHanNumeral || + isNavigateKey + ) { + return; + } - const amount = getStepAmount() ?? DEFAULT_STEP; + // We need to commit the number at this point if the input hasn't been blurred. + const parsedValue = parseNumber(inputValue, formatOptionsRef.current); - // Prevent insertion of text or caret from moving. - event.preventDefault(); + const amount = getStepAmount() ?? DEFAULT_STEP; - if (event.key === 'ArrowUp') { - incrementValue(amount, 1, parsedValue); - } else if (event.key === 'ArrowDown') { - incrementValue(amount, -1, parsedValue); - } else if (event.key === 'Home' && min != null) { - setValue(min); - } else if (event.key === 'End' && max != null) { - setValue(max); - } - }, - onPaste(event) { - externalProps.onPaste?.(event); - if (event.defaultPrevented || readOnly || disabled) { - return; - } + // Prevent insertion of text or caret from moving. + event.preventDefault(); + + if (event.key === 'ArrowUp') { + incrementValue(amount, 1, parsedValue); + } else if (event.key === 'ArrowDown') { + incrementValue(amount, -1, parsedValue); + } else if (event.key === 'Home' && min != null) { + setValue(min); + } else if (event.key === 'End' && max != null) { + setValue(max); + } + }, + onPaste(event) { + if (event.defaultPrevented || readOnly || disabled) { + return; + } - // Prevent `onChange` from being called. - event.preventDefault(); + // Prevent `onChange` from being called. + event.preventDefault(); - const clipboardData = event.clipboardData || window.Clipboard; - const pastedData = clipboardData.getData('text/plain'); - const parsedValue = parseNumber(pastedData, formatOptionsRef.current); + const clipboardData = event.clipboardData || window.Clipboard; + const pastedData = clipboardData.getData('text/plain'); + const parsedValue = parseNumber(pastedData, formatOptionsRef.current); - if (parsedValue !== null) { - allowInputSyncRef.current = false; - setValue(parsedValue); - setInputValue(pastedData); - } - }, - }), + if (parsedValue !== null) { + allowInputSyncRef.current = false; + setValue(parsedValue); + setInputValue(pastedData); + } + }, + }), [ id, required, diff --git a/packages/mui-base/src/useNumberField/useScrub.ts b/packages/mui-base/src/useNumberField/useScrub.ts index f38f2f6878..a7402074f4 100644 --- a/packages/mui-base/src/useNumberField/useScrub.ts +++ b/packages/mui-base/src/useNumberField/useScrub.ts @@ -7,6 +7,7 @@ import { isWebKit } from '../utils/detectBrowser'; import { DEFAULT_STEP } from './constants'; import { ScrubHandle, ScrubParams } from './useScrub.types'; import { getViewportRect, subscribeToVisualViewportResize } from './utils'; +import { mergeReactProps } from '../utils/mergeReactProps'; /** * @ignore - internal hook. @@ -107,41 +108,39 @@ export function useScrub(params: ScrubParams) { ); const getScrubAreaProps: UseNumberFieldReturnValue['getScrubAreaProps'] = React.useCallback( - (externalProps = {}) => ({ - role: 'presentation', - ['data-scrubbing' as string]: isScrubbing || undefined, - ...externalProps, - style: { - touchAction: 'none', - WebkitUserSelect: 'none', - userSelect: 'none', - ...externalProps.style, - }, - onPointerDown(event) { - externalProps.onPointerDown?.(event); - const isMainButton = !event.button || event.button === 0; - if (event.defaultPrevented || readOnly || !isMainButton || disabled) { - return; - } - - if (event.pointerType === 'mouse') { - event.preventDefault(); - inputRef.current?.focus(); - } - - isScrubbingRef.current = true; - onScrubbingChange(true, event.nativeEvent); - - // WebKit causes significant layout shift with the native message, so we can't use it. - if (!isWebKit()) { - // There can be some frames where there's no cursor at all when requesting the pointer lock. - // This is a workaround to avoid flickering. - avoidFlickerTimeoutRef.current = window.setTimeout(() => { - ownerDocument(scrubAreaRef.current).body.requestPointerLock?.(); - }, 20); - } - }, - }), + (externalProps = {}) => + mergeReactProps<'span'>(externalProps, { + role: 'presentation', + ['data-scrubbing' as string]: isScrubbing || undefined, + style: { + touchAction: 'none', + WebkitUserSelect: 'none', + userSelect: 'none', + }, + onPointerDown(event) { + const isMainButton = !event.button || event.button === 0; + if (event.defaultPrevented || readOnly || !isMainButton || disabled) { + return; + } + + if (event.pointerType === 'mouse') { + event.preventDefault(); + inputRef.current?.focus(); + } + + isScrubbingRef.current = true; + onScrubbingChange(true, event.nativeEvent); + + // WebKit causes significant layout shift with the native message, so we can't use it. + if (!isWebKit()) { + // There can be some frames where there's no cursor at all when requesting the pointer lock. + // This is a workaround to avoid flickering. + avoidFlickerTimeoutRef.current = window.setTimeout(() => { + ownerDocument(scrubAreaRef.current).body.requestPointerLock?.(); + }, 20); + } + }, + }), [readOnly, disabled, onScrubbingChange, inputRef, isScrubbing], ); @@ -149,14 +148,12 @@ export function useScrub(params: ScrubParams) { React.useCallback( (externalProps = {}) => ({ role: 'presentation', - ...externalProps, style: { position: 'fixed', top: 0, left: 0, pointerEvents: 'none', zIndex: 2147483647, // max z-index - ...cursorStyles, ...externalProps.style, transform: `${cursorStyles.transform} ${externalProps.style?.transform || ''}`, }, diff --git a/packages/mui-base/src/utils/defineProps.ts b/packages/mui-base/src/utils/defineProps.ts deleted file mode 100644 index c51ca23a90..0000000000 --- a/packages/mui-base/src/utils/defineProps.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function defineProps>( - props: React.ComponentPropsWithRef, -) { - return props; -} diff --git a/packages/mui-base/src/utils/mergeReactProps.ts b/packages/mui-base/src/utils/mergeReactProps.ts index 1d78730da8..4a34eff94b 100644 --- a/packages/mui-base/src/utils/mergeReactProps.ts +++ b/packages/mui-base/src/utils/mergeReactProps.ts @@ -12,7 +12,7 @@ import type { BaseUIEvent, WithBaseUIEvent } from './BaseUI.types'; * @returns the merged props. */ export function mergeReactProps( - externalProps: WithBaseUIEvent>, + externalProps: WithBaseUIEvent>, internalProps: React.ComponentPropsWithRef, ): WithBaseUIEvent> { return Object.entries(externalProps).reduce(