From 924e712745e234f47aa0a31b768542926a46dc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 16:06:50 +0800 Subject: [PATCH 01/10] refactor: simplify dom --- assets/index.less | 43 +- docs/demo/prefix-suffix.tsx | 25 + docs/example.md | 4 + src/InputNumber.tsx | 1113 ++++++++++--------- src/StepHandler.tsx | 24 +- tests/__snapshots__/baseInput.test.tsx.snap | 227 ++-- 6 files changed, 706 insertions(+), 730 deletions(-) create mode 100644 docs/demo/prefix-suffix.tsx diff --git a/assets/index.less b/assets/index.less index ed5ab089..08cebed8 100644 --- a/assets/index.less +++ b/assets/index.less @@ -1,7 +1,8 @@ @inputNumberPrefixCls: rc-input-number; .@{inputNumberPrefixCls} { - display: inline-block; + display: inline-flex; + flex-wrap: nowrap; height: 26px; margin: 0; padding: 0; @@ -22,7 +23,7 @@ } } - &-handler { + &-action { display: block; height: 12px; overflow: hidden; @@ -35,8 +36,8 @@ } } - &-handler-up-inner, - &-handler-down-inner { + &-action-up-inner, + &-action-down-inner { color: #666666; -webkit-user-select: none; user-select: none; @@ -45,8 +46,8 @@ &:hover { border-color: #1890ff; - .@{inputNumberPrefixCls}-handler-up, - .@{inputNumberPrefixCls}-handler-wrap { + .@{inputNumberPrefixCls}-action-up, + .@{inputNumberPrefixCls}-actions { border-color: #1890ff; } } @@ -54,17 +55,12 @@ &-disabled:hover { border-color: #d9d9d9; - .@{inputNumberPrefixCls}-handler-up, - .@{inputNumberPrefixCls}-handler-wrap { + .@{inputNumberPrefixCls}-action-up, + .@{inputNumberPrefixCls}-actions { border-color: #d9d9d9; } } - &-input-wrap { - height: 100%; - overflow: hidden; - } - &-input { width: 100%; height: 100%; @@ -80,15 +76,14 @@ -moz-appearance: textfield; } - &-handler-wrap { - float: right; + &-actions { width: 20px; height: 100%; border-left: 1px solid #d9d9d9; transition: all 0.3s; } - &-handler-up { + &-action-up { padding-top: 1px; border-bottom: 1px solid #d9d9d9; transition: all 0.3s; @@ -100,7 +95,7 @@ } } - &-handler-down { + &-action-down { transition: all 0.3s; &-inner { @@ -118,8 +113,8 @@ } } - &-handler-down-disabled, - &-handler-up-disabled { + &-action-down-disabled, + &-action-up-disabled { .handler-disabled(); } @@ -129,7 +124,7 @@ cursor: not-allowed; opacity: 0.72; } - .@{inputNumberPrefixCls}-handler { + .@{inputNumberPrefixCls}-action { .handler-disabled(); } } @@ -139,17 +134,17 @@ align-items: center; } - &-mode-spinner &-handler { + &-mode-spinner &-action { flex: 0 0 20px; - line-height: 26px; height: 100%; + line-height: 26px; } - &-mode-spinner &-handler-up { + &-mode-spinner &-action-up { border-bottom: 0; border-left: 1px solid #d9d9d9; } - &-mode-spinner &-handler-down { + &-mode-spinner &-action-down { border-top: 0; border-right: 1px solid #d9d9d9; } diff --git a/docs/demo/prefix-suffix.tsx b/docs/demo/prefix-suffix.tsx new file mode 100644 index 00000000..b41319b3 --- /dev/null +++ b/docs/demo/prefix-suffix.tsx @@ -0,0 +1,25 @@ +/* eslint no-console:0 */ +import InputNumber from '@rc-component/input-number'; +import React from 'react'; +import '../../assets/index.less'; + +export default () => { + const [value, setValue] = React.useState(100); + + const onChange = (val: number) => { + console.warn('onChange:', val, typeof val); + setValue(val); + }; + + return ( +
+ +
+ ); +}; diff --git a/docs/example.md b/docs/example.md index b88e7135..7b2ec5d6 100644 --- a/docs/example.md +++ b/docs/example.md @@ -9,6 +9,10 @@ nav: +## prefix-suffix + + + ## combination-key-format diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index e1ed2514..28c63a77 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -1,4 +1,3 @@ -import { BaseInput } from '@rc-component/input'; import getMiniDecimal, { DecimalClass, getNumberPrecision, @@ -9,7 +8,6 @@ import getMiniDecimal, { } from '@rc-component/mini-decimal'; import { useLayoutUpdateEffect } from '@rc-component/util/lib/hooks/useLayoutEffect'; import proxyObject from '@rc-component/util/lib/proxyObject'; -import { composeRef } from '@rc-component/util/lib/ref'; import { clsx } from 'clsx'; import * as React from 'react'; import useCursor from './hooks/useCursor'; @@ -17,9 +15,8 @@ import SemanticContext from './SemanticContext'; import StepHandler from './StepHandler'; import { getDecupleSteps } from './utils/numberUtil'; -import type { HolderRef } from '@rc-component/input/lib/BaseInput'; import { BaseInputProps } from '@rc-component/input/lib/interface'; -import { InputFocusOptions, triggerFocus } from '@rc-component/input/lib/utils/commonUtils'; +import { InputFocusOptions } from '@rc-component/input/lib/utils/commonUtils'; import { useEvent } from '@rc-component/util'; import useFrame from './hooks/useFrame'; @@ -115,636 +112,646 @@ export interface InputNumberProps changeOnBlur?: boolean; } -type InternalInputNumberProps = Omit & { - domRef: React.Ref; -}; - -const InternalInputNumber = React.forwardRef( - (props: InternalInputNumberProps, ref: React.Ref) => { - const { - mode, - prefixCls, - className, - style, - min, - max, - step = 1, - defaultValue, - value, - disabled, - readOnly, - upHandler, - downHandler, - keyboard, - changeOnWheel = false, - controls = true, - - stringMode, - - parser, - formatter, - precision, - decimalSeparator, - - onChange, - onInput, - onPressEnter, - onStep, - - changeOnBlur = true, - - domRef, - - ...inputProps - } = props; - - const { classNames, styles } = React.useContext(SemanticContext) || {}; - - const inputClassName = `${prefixCls}-input`; - - const inputRef = React.useRef(null); - - const [focus, setFocus] = React.useState(false); - - const userTypingRef = React.useRef(false); - const compositionRef = React.useRef(false); - const shiftKeyRef = React.useRef(false); - - // ============================ Value ============================= - // Real value control - const [decimalValue, setDecimalValue] = React.useState(() => - getMiniDecimal(value ?? defaultValue), - ); - - function setUncontrolledDecimalValue(newDecimal: DecimalClass) { - if (value === undefined) { - setDecimalValue(newDecimal); - } - } +const InputNumber = React.forwardRef((props, ref) => { + const { + mode = 'input', + prefixCls = 'rc-input-number', + className, + style, + min, + max, + step = 1, + defaultValue, + value, + disabled, + readOnly, + upHandler, + downHandler, + keyboard, + changeOnWheel = false, + controls = true, - // ====================== Parser & Formatter ====================== - /** - * `precision` is used for formatter & onChange. - * It will auto generate by `value` & `step`. - * But it will not block user typing. - * - * Note: Auto generate `precision` is used for legacy logic. - * We should remove this since we already support high precision with BigInt. - * - * @param number Provide which number should calculate precision - * @param userTyping Change by user typing - */ - const getPrecision = React.useCallback( - (numStr: string, userTyping: boolean) => { - if (userTyping) { - return undefined; - } + prefix, + suffix, + stringMode, - if (precision >= 0) { - return precision; - } + parser, + formatter, + precision, + decimalSeparator, - return Math.max(getNumberPrecision(numStr), getNumberPrecision(step)); - }, - [precision, step], - ); + onChange, + onInput, + onPressEnter, + onStep, - // >>> Parser - const mergedParser = React.useCallback( - (num: string | number) => { - const numStr = String(num); + changeOnBlur = true, - if (parser) { - return parser(numStr); - } + ...inputProps + } = props; - let parsedStr = numStr; - if (decimalSeparator) { - parsedStr = parsedStr.replace(decimalSeparator, '.'); - } + const { classNames, styles } = React.useContext(SemanticContext) || {}; - // [Legacy] We still support auto convert `$ 123,456` to `123456` - return parsedStr.replace(/[^\w.-]+/g, ''); - }, - [parser, decimalSeparator], - ); - - // >>> Formatter - const inputValueRef = React.useRef(''); - const mergedFormatter = React.useCallback( - (number: string, userTyping: boolean) => { - if (formatter) { - return formatter(number, { userTyping, input: String(inputValueRef.current) }); - } + const inputClassName = `${prefixCls}-input`; - let str = typeof number === 'number' ? num2str(number) : number; + const [focus, setFocus] = React.useState(false); - // User typing will not auto format with precision directly - if (!userTyping) { - const mergedPrecision = getPrecision(str, userTyping); + const userTypingRef = React.useRef(false); + const compositionRef = React.useRef(false); + const shiftKeyRef = React.useRef(false); - if (validateNumber(str) && (decimalSeparator || mergedPrecision >= 0)) { - // Separator - const separatorStr = decimalSeparator || '.'; + // ============================= Refs ============================= + const rootRef = React.useRef(null); + const inputRef = React.useRef(null); - str = toFixed(str, separatorStr, mergedPrecision); - } - } + React.useImperativeHandle(ref, () => + proxyObject(inputRef.current, { + focus, + nativeElement: rootRef.current, + }), + ); - return str; - }, - [formatter, getPrecision, decimalSeparator], - ); + // ============================ Value ============================= + // Real value control + const [decimalValue, setDecimalValue] = React.useState(() => + getMiniDecimal(value ?? defaultValue), + ); - // ========================== InputValue ========================== - /** - * Input text value control - * - * User can not update input content directly. It updates with follow rules by priority: - * 1. controlled `value` changed - * * [SPECIAL] Typing like `1.` should not immediately convert to `1` - * 2. User typing with format (not precision) - * 3. Blur or Enter trigger revalidate - */ - const [inputValue, setInternalInputValue] = React.useState(() => { - const initValue = defaultValue ?? value; - if (decimalValue.isInvalidate() && ['string', 'number'].includes(typeof initValue)) { - return Number.isNaN(initValue) ? '' : initValue; - } - return mergedFormatter(decimalValue.toString(), false); - }); - inputValueRef.current = inputValue; - - // Should always be string - function setInputValue(newValue: DecimalClass, userTyping: boolean) { - setInternalInputValue( - mergedFormatter( - // Invalidate number is sometime passed by external control, we should let it go - // Otherwise is controlled by internal interactive logic which check by userTyping - // You can ref 'show limited value when input is not focused' test for more info. - newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping), - userTyping, - ), - ); + function setUncontrolledDecimalValue(newDecimal: DecimalClass) { + if (value === undefined) { + setDecimalValue(newDecimal); } + } - // >>> Max & Min limit - const maxDecimal = React.useMemo(() => getDecimalIfValidate(max), [max, precision]); - const minDecimal = React.useMemo(() => getDecimalIfValidate(min), [min, precision]); + // ====================== Parser & Formatter ====================== + /** + * `precision` is used for formatter & onChange. + * It will auto generate by `value` & `step`. + * But it will not block user typing. + * + * Note: Auto generate `precision` is used for legacy logic. + * We should remove this since we already support high precision with BigInt. + * + * @param number Provide which number should calculate precision + * @param userTyping Change by user typing + */ + const getPrecision = React.useCallback( + (numStr: string, userTyping: boolean) => { + if (userTyping) { + return undefined; + } - const upDisabled = React.useMemo(() => { - if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) { - return false; + if (precision >= 0) { + return precision; } - return maxDecimal.lessEquals(decimalValue); - }, [maxDecimal, decimalValue]); + return Math.max(getNumberPrecision(numStr), getNumberPrecision(step)); + }, + [precision, step], + ); + + // >>> Parser + const mergedParser = React.useCallback( + (num: string | number) => { + const numStr = String(num); - const downDisabled = React.useMemo(() => { - if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) { - return false; + if (parser) { + return parser(numStr); } - return decimalValue.lessEquals(minDecimal); - }, [minDecimal, decimalValue]); - - // Cursor controller - const [recordCursor, restoreCursor] = useCursor(inputRef.current, focus); - - // ============================= Data ============================= - /** - * Find target value closet within range. - * e.g. [11, 28]: - * 3 => 11 - * 23 => 23 - * 99 => 28 - */ - const getRangeValue = (target: DecimalClass) => { - // target > max - if (maxDecimal && !target.lessEquals(maxDecimal)) { - return maxDecimal; + let parsedStr = numStr; + if (decimalSeparator) { + parsedStr = parsedStr.replace(decimalSeparator, '.'); } - // target < min - if (minDecimal && !minDecimal.lessEquals(target)) { - return minDecimal; - } + // [Legacy] We still support auto convert `$ 123,456` to `123456` + return parsedStr.replace(/[^\w.-]+/g, ''); + }, + [parser, decimalSeparator], + ); - return null; - }; - - /** - * Check value is in [min, max] range - */ - const isInRange = (target: DecimalClass) => !getRangeValue(target); - - /** - * Trigger `onChange` if value validated and not equals of origin. - * Return the value that re-align in range. - */ - const triggerValueUpdate = (newValue: DecimalClass, userTyping: boolean): DecimalClass => { - let updateValue = newValue; - - let isRangeValidate = isInRange(updateValue) || updateValue.isEmpty(); - - // Skip align value when trigger value is empty. - // We just trigger onChange(null) - // This should not block user typing - if (!updateValue.isEmpty() && !userTyping) { - // Revert value in range if needed - updateValue = getRangeValue(updateValue) || updateValue; - isRangeValidate = true; + // >>> Formatter + const inputValueRef = React.useRef(''); + const mergedFormatter = React.useCallback( + (number: string, userTyping: boolean) => { + if (formatter) { + return formatter(number, { userTyping, input: String(inputValueRef.current) }); } - if (!readOnly && !disabled && isRangeValidate) { - const numStr = updateValue.toString(); - const mergedPrecision = getPrecision(numStr, userTyping); - if (mergedPrecision >= 0) { - updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision)); - - // When to fixed. The value may out of min & max range. - // 4 in [0, 3.8] => 3.8 => 4 (toFixed) - if (!isInRange(updateValue)) { - updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision, true)); - } - } + let str = typeof number === 'number' ? num2str(number) : number; - // Trigger event - if (!updateValue.equals(decimalValue)) { - setUncontrolledDecimalValue(updateValue); - onChange?.(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue)); + // User typing will not auto format with precision directly + if (!userTyping) { + const mergedPrecision = getPrecision(str, userTyping); - // Reformat input if value is not controlled - if (value === undefined) { - setInputValue(updateValue, userTyping); - } - } + if (validateNumber(str) && (decimalSeparator || mergedPrecision >= 0)) { + // Separator + const separatorStr = decimalSeparator || '.'; - return updateValue; + str = toFixed(str, separatorStr, mergedPrecision); + } } - return decimalValue; - }; + return str; + }, + [formatter, getPrecision, decimalSeparator], + ); - // ========================== User Input ========================== - const onNextPromise = useFrame(); + // ========================== InputValue ========================== + /** + * Input text value control + * + * User can not update input content directly. It updates with follow rules by priority: + * 1. controlled `value` changed + * * [SPECIAL] Typing like `1.` should not immediately convert to `1` + * 2. User typing with format (not precision) + * 3. Blur or Enter trigger revalidate + */ + const [inputValue, setInternalInputValue] = React.useState(() => { + const initValue = defaultValue ?? value; + if (decimalValue.isInvalidate() && ['string', 'number'].includes(typeof initValue)) { + return Number.isNaN(initValue) ? '' : initValue; + } + return mergedFormatter(decimalValue.toString(), false); + }); + inputValueRef.current = inputValue; + + // Should always be string + function setInputValue(newValue: DecimalClass, userTyping: boolean) { + setInternalInputValue( + mergedFormatter( + // Invalidate number is sometime passed by external control, we should let it go + // Otherwise is controlled by internal interactive logic which check by userTyping + // You can ref 'show limited value when input is not focused' test for more info. + newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping), + userTyping, + ), + ); + } - // >>> Collect input value - const collectInputValue = (inputStr: string) => { - recordCursor(); + // >>> Max & Min limit + const maxDecimal = React.useMemo(() => getDecimalIfValidate(max), [max, precision]); + const minDecimal = React.useMemo(() => getDecimalIfValidate(min), [min, precision]); - // Update inputValue in case input can not parse as number - // Refresh ref value immediately since it may used by formatter - inputValueRef.current = inputStr; - setInternalInputValue(inputStr); + const upDisabled = React.useMemo(() => { + if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) { + return false; + } - // Parse number - if (!compositionRef.current) { - const finalValue = mergedParser(inputStr); - const finalDecimal = getMiniDecimal(finalValue); - if (!finalDecimal.isNaN()) { - triggerValueUpdate(finalDecimal, true); - } - } + return maxDecimal.lessEquals(decimalValue); + }, [maxDecimal, decimalValue]); - // Trigger onInput later to let user customize value if they want to handle something after onChange - onInput?.(inputStr); + const downDisabled = React.useMemo(() => { + if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) { + return false; + } - // optimize for chinese input experience - // https://github.com/ant-design/ant-design/issues/8196 - onNextPromise(() => { - let nextInputStr = inputStr; - if (!parser) { - nextInputStr = inputStr.replace(/。/g, '.'); - } + return decimalValue.lessEquals(minDecimal); + }, [minDecimal, decimalValue]); - if (nextInputStr !== inputStr) { - collectInputValue(nextInputStr); - } - }); - }; - - // >>> Composition - const onCompositionStart = () => { - compositionRef.current = true; - }; - - const onCompositionEnd = () => { - compositionRef.current = false; - - collectInputValue(inputRef.current.value); - }; - - // >>> Input - const onInternalInput: React.ChangeEventHandler = (e) => { - collectInputValue(e.target.value); - }; - - // ============================= Step ============================= - const onInternalStep = useEvent((up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => { - // Ignore step since out of range - if ((up && upDisabled) || (!up && downDisabled)) { - return; - } + // Cursor controller + const [recordCursor, restoreCursor] = useCursor(inputRef.current, focus); - // Clear typing status since it may be caused by up & down key. - // We should sync with input value. - userTypingRef.current = false; + // ============================= Data ============================= + /** + * Find target value closet within range. + * e.g. [11, 28]: + * 3 => 11 + * 23 => 23 + * 99 => 28 + */ + const getRangeValue = (target: DecimalClass) => { + // target > max + if (maxDecimal && !target.lessEquals(maxDecimal)) { + return maxDecimal; + } - let stepDecimal = getMiniDecimal(shiftKeyRef.current ? getDecupleSteps(step) : step); - if (!up) { - stepDecimal = stepDecimal.negate(); - } + // target < min + if (minDecimal && !minDecimal.lessEquals(target)) { + return minDecimal; + } - const target = (decimalValue || getMiniDecimal(0)).add(stepDecimal.toString()); + return null; + }; - const updatedValue = triggerValueUpdate(target, false); + /** + * Check value is in [min, max] range + */ + const isInRange = (target: DecimalClass) => !getRangeValue(target); - onStep?.(getDecimalValue(stringMode, updatedValue), { - offset: shiftKeyRef.current ? getDecupleSteps(step) : step, - type: up ? 'up' : 'down', - emitter, - }); + /** + * Trigger `onChange` if value validated and not equals of origin. + * Return the value that re-align in range. + */ + const triggerValueUpdate = (newValue: DecimalClass, userTyping: boolean): DecimalClass => { + let updateValue = newValue; + + let isRangeValidate = isInRange(updateValue) || updateValue.isEmpty(); + + // Skip align value when trigger value is empty. + // We just trigger onChange(null) + // This should not block user typing + if (!updateValue.isEmpty() && !userTyping) { + // Revert value in range if needed + updateValue = getRangeValue(updateValue) || updateValue; + isRangeValidate = true; + } - inputRef.current?.focus(); - }); + if (!readOnly && !disabled && isRangeValidate) { + const numStr = updateValue.toString(); + const mergedPrecision = getPrecision(numStr, userTyping); + if (mergedPrecision >= 0) { + updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision)); - // ============================ Flush ============================= - /** - * Flush current input content to trigger value change & re-formatter input if needed. - * This will always flush input value for update. - * If it's invalidate, will fallback to last validate value. - */ - const flushInputValue = (userTyping: boolean) => { - const parsedValue = getMiniDecimal(mergedParser(inputValue)); - let formatValue: DecimalClass; - - if (!parsedValue.isNaN()) { - // Only validate value or empty value can be re-fill to inputValue - // Reassign the formatValue within ranged of trigger control - formatValue = triggerValueUpdate(parsedValue, userTyping); - } else { - formatValue = triggerValueUpdate(decimalValue, userTyping); + // When to fixed. The value may out of min & max range. + // 4 in [0, 3.8] => 3.8 => 4 (toFixed) + if (!isInRange(updateValue)) { + updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision, true)); + } } - if (value !== undefined) { - // Reset back with controlled value first - setInputValue(decimalValue, false); - } else if (!formatValue.isNaN()) { - // Reset input back since no validate value - setInputValue(formatValue, false); + // Trigger event + if (!updateValue.equals(decimalValue)) { + setUncontrolledDecimalValue(updateValue); + onChange?.(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue)); + + // Reformat input if value is not controlled + if (value === undefined) { + setInputValue(updateValue, userTyping); + } } - }; - // Solve the issue of the event triggering sequence when entering numbers in chinese input (Safari) - const onBeforeInput = () => { - userTypingRef.current = true; - }; + return updateValue; + } - const onKeyDown: React.KeyboardEventHandler = (event) => { - const { key, shiftKey } = event; - userTypingRef.current = true; + return decimalValue; + }; - shiftKeyRef.current = shiftKey; + // ========================== User Input ========================== + const onNextPromise = useFrame(); - if (key === 'Enter') { - if (!compositionRef.current) { - userTypingRef.current = false; - } - flushInputValue(false); - onPressEnter?.(event); - } + // >>> Collect input value + const collectInputValue = (inputStr: string) => { + recordCursor(); + + // Update inputValue in case input can not parse as number + // Refresh ref value immediately since it may used by formatter + inputValueRef.current = inputStr; + setInternalInputValue(inputStr); - if (keyboard === false) { - return; + // Parse number + if (!compositionRef.current) { + const finalValue = mergedParser(inputStr); + const finalDecimal = getMiniDecimal(finalValue); + if (!finalDecimal.isNaN()) { + triggerValueUpdate(finalDecimal, true); } + } - // Do step - if (!compositionRef.current && ['Up', 'ArrowUp', 'Down', 'ArrowDown'].includes(key)) { - onInternalStep(key === 'Up' || key === 'ArrowUp', 'keyboard'); - event.preventDefault(); + // Trigger onInput later to let user customize value if they want to handle something after onChange + onInput?.(inputStr); + + // optimize for chinese input experience + // https://github.com/ant-design/ant-design/issues/8196 + onNextPromise(() => { + let nextInputStr = inputStr; + if (!parser) { + nextInputStr = inputStr.replace(/。/g, '.'); } - }; - - const onKeyUp = () => { - userTypingRef.current = false; - shiftKeyRef.current = false; - }; - - React.useEffect(() => { - if (changeOnWheel && focus) { - const onWheel = (event) => { - // moving mouse wheel rises wheel event with deltaY < 0 - // scroll value grows from top to bottom, as screen Y coordinate - onInternalStep(event.deltaY < 0, 'wheel'); - event.preventDefault(); - }; - const input = inputRef.current; - if (input) { - // React onWheel is passive and we can't preventDefault() in it. - // That's why we should subscribe with DOM listener - // https://stackoverflow.com/questions/63663025/react-onwheel-handler-cant-preventdefault-because-its-a-passive-event-listenev - input.addEventListener('wheel', onWheel, { passive: false }); - return () => input.removeEventListener('wheel', onWheel); - } + + if (nextInputStr !== inputStr) { + collectInputValue(nextInputStr); } }); + }; - // >>> Focus & Blur - const onBlur = () => { - if (changeOnBlur) { - flushInputValue(false); - } + // >>> Composition + const onCompositionStart = () => { + compositionRef.current = true; + }; - setFocus(false); + const onCompositionEnd = () => { + compositionRef.current = false; - userTypingRef.current = false; - }; + collectInputValue(inputRef.current.value); + }; - // ========================== Controlled ========================== - // Input by precision & formatter - useLayoutUpdateEffect(() => { - if (!decimalValue.isInvalidate()) { - setInputValue(decimalValue, false); - } - }, [precision, formatter]); + // >>> Input + const onInternalInput: React.ChangeEventHandler = (e) => { + collectInputValue(e.target.value); + }; + + // ============================= Step ============================= + const onInternalStep = useEvent((up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => { + // Ignore step since out of range + if ((up && upDisabled) || (!up && downDisabled)) { + return; + } - // Input by value - useLayoutUpdateEffect(() => { - const newValue = getMiniDecimal(value); - setDecimalValue(newValue); + // Clear typing status since it may be caused by up & down key. + // We should sync with input value. + userTypingRef.current = false; - const currentParsedValue = getMiniDecimal(mergedParser(inputValue)); + let stepDecimal = getMiniDecimal(shiftKeyRef.current ? getDecupleSteps(step) : step); + if (!up) { + stepDecimal = stepDecimal.negate(); + } + + const target = (decimalValue || getMiniDecimal(0)).add(stepDecimal.toString()); + + const updatedValue = triggerValueUpdate(target, false); + + onStep?.(getDecimalValue(stringMode, updatedValue), { + offset: shiftKeyRef.current ? getDecupleSteps(step) : step, + type: up ? 'up' : 'down', + emitter, + }); - // When user typing from `1.2` to `1.`, we should not convert to `1` immediately. - // But let it go if user set `formatter` - if (!newValue.equals(currentParsedValue) || !userTypingRef.current || formatter) { - // Update value as effect - setInputValue(newValue, userTypingRef.current); + inputRef.current?.focus(); + }); + + // ============================ Flush ============================= + /** + * Flush current input content to trigger value change & re-formatter input if needed. + * This will always flush input value for update. + * If it's invalidate, will fallback to last validate value. + */ + const flushInputValue = (userTyping: boolean) => { + const parsedValue = getMiniDecimal(mergedParser(inputValue)); + let formatValue: DecimalClass; + + if (!parsedValue.isNaN()) { + // Only validate value or empty value can be re-fill to inputValue + // Reassign the formatValue within ranged of trigger control + formatValue = triggerValueUpdate(parsedValue, userTyping); + } else { + formatValue = triggerValueUpdate(decimalValue, userTyping); + } + + if (value !== undefined) { + // Reset back with controlled value first + setInputValue(decimalValue, false); + } else if (!formatValue.isNaN()) { + // Reset input back since no validate value + setInputValue(formatValue, false); + } + }; + + // Solve the issue of the event triggering sequence when entering numbers in chinese input (Safari) + const onBeforeInput = () => { + userTypingRef.current = true; + }; + + const onKeyDown: React.KeyboardEventHandler = (event) => { + const { key, shiftKey } = event; + userTypingRef.current = true; + + shiftKeyRef.current = shiftKey; + + if (key === 'Enter') { + if (!compositionRef.current) { + userTypingRef.current = false; } - }, [value]); + flushInputValue(false); + onPressEnter?.(event); + } - // ============================ Cursor ============================ - useLayoutUpdateEffect(() => { - if (formatter) { - restoreCursor(); + if (keyboard === false) { + return; + } + + // Do step + if (!compositionRef.current && ['Up', 'ArrowUp', 'Down', 'ArrowDown'].includes(key)) { + onInternalStep(key === 'Up' || key === 'ArrowUp', 'keyboard'); + event.preventDefault(); + } + }; + + const onKeyUp = () => { + userTypingRef.current = false; + shiftKeyRef.current = false; + }; + + React.useEffect(() => { + if (changeOnWheel && focus) { + const onWheel = (event) => { + // moving mouse wheel rises wheel event with deltaY < 0 + // scroll value grows from top to bottom, as screen Y coordinate + onInternalStep(event.deltaY < 0, 'wheel'); + event.preventDefault(); + }; + const input = inputRef.current; + if (input) { + // React onWheel is passive and we can't preventDefault() in it. + // That's why we should subscribe with DOM listener + // https://stackoverflow.com/questions/63663025/react-onwheel-handler-cant-preventdefault-because-its-a-passive-event-listenev + input.addEventListener('wheel', onWheel, { passive: false }); + return () => input.removeEventListener('wheel', onWheel); } - }, [inputValue]); - - // ============================ Render ============================ - // >>>>>> Handler - const sharedHandlerProps = { - prefixCls, - onStep: onInternalStep, - className: classNames?.action, - style: styles?.action, - }; - - const upNode = ( - - {upHandler} - - ); + } + }); - const downNode = ( - - {downHandler} - - ); + // >>> Focus & Blur + const onBlur = () => { + if (changeOnBlur) { + flushInputValue(false); + } - // >>>>>> Render - return ( -
{ - setFocus(true); - }} - onBlur={onBlur} - onKeyDown={onKeyDown} - onKeyUp={onKeyUp} - onCompositionStart={onCompositionStart} - onCompositionEnd={onCompositionEnd} - onBeforeInput={onBeforeInput} - > - {mode === 'input' && controls && ( -
- {upNode} - {downNode} -
- )} - - {mode === 'spinner' && controls && downNode} - -
- -
+ setFocus(false); - {mode === 'spinner' && controls && upNode} -
- ); - }, -); + userTypingRef.current = false; + }; -const InputNumber = React.forwardRef((props, ref) => { - const { - mode = 'input', - disabled, - style, - prefixCls = 'rc-input-number', - value, - prefix, - suffix, - addonBefore, - addonAfter, - className, - classNames, - styles, - ...rest - } = props; + // ========================== Controlled ========================== + // Input by precision & formatter + useLayoutUpdateEffect(() => { + if (!decimalValue.isInvalidate()) { + setInputValue(decimalValue, false); + } + }, [precision, formatter]); + + // Input by value + useLayoutUpdateEffect(() => { + const newValue = getMiniDecimal(value); + setDecimalValue(newValue); - const holderRef = React.useRef(null); - const inputNumberDomRef = React.useRef(null); - const inputFocusRef = React.useRef(null); + const currentParsedValue = getMiniDecimal(mergedParser(inputValue)); - const focus = (option?: InputFocusOptions) => { - if (inputFocusRef.current) { - triggerFocus(inputFocusRef.current, option); + // When user typing from `1.2` to `1.`, we should not convert to `1` immediately. + // But let it go if user set `formatter` + if (!newValue.equals(currentParsedValue) || !userTypingRef.current || formatter) { + // Update value as effect + setInputValue(newValue, userTypingRef.current); } + }, [value]); + + // ============================ Cursor ============================ + useLayoutUpdateEffect(() => { + if (formatter) { + restoreCursor(); + } + }, [inputValue]); + + // ============================ Render ============================ + // >>>>>> Handler + const sharedHandlerProps = { + prefixCls, + onStep: onInternalStep, + className: classNames?.action, + style: styles?.action, }; - React.useImperativeHandle(ref, () => - proxyObject(inputFocusRef.current, { - focus, - nativeElement: holderRef.current.nativeElement || inputNumberDomRef.current, - }), + const upNode = ( + + {upHandler} + ); - const memoizedValue = React.useMemo(() => ({ classNames, styles }), [classNames, styles]); + + const downNode = ( + + {downHandler} + + ); + + // >>>>>> Render return ( - - { + setFocus(true); + }} + onBlur={onBlur} + onKeyDown={onKeyDown} + onKeyUp={onKeyUp} + onCompositionStart={onCompositionStart} + onCompositionEnd={onCompositionEnd} + onBeforeInput={onBeforeInput} + > + {mode === 'spinner' && controls && downNode} + + {suffix !== undefined && ( +
+ {suffix} +
+ )} + + - -
-
+ readOnly={readOnly} + /> + + {suffix !== undefined && ( +
+ {prefix} +
+ )} + + {mode === 'spinner' && controls && upNode} + + {mode === 'input' && controls && ( +
+ {upNode} + {downNode} +
+ )} + ); -}) as (( - props: React.PropsWithChildren> & { - ref?: React.Ref; - }, -) => React.ReactElement) & { displayName?: string }; +}); + +// const InputNumber = React.forwardRef((props, ref) => { +// const { +// mode = 'input', +// disabled, +// style, +// prefixCls = 'rc-input-number', +// value, +// prefix, +// suffix, +// addonBefore, +// addonAfter, +// className, +// classNames, +// styles, +// ...rest +// } = props; + +// const holderRef = React.useRef(null); +// const inputNumberDomRef = React.useRef(null); +// const inputFocusRef = React.useRef(null); + +// const focus = (option?: InputFocusOptions) => { +// if (inputFocusRef.current) { +// triggerFocus(inputFocusRef.current, option); +// } +// }; + +// React.useImperativeHandle(ref, () => +// proxyObject(inputFocusRef.current, { +// focus, +// nativeElement: holderRef.current.nativeElement || inputNumberDomRef.current, +// }), +// ); +// const memoizedValue = React.useMemo(() => ({ classNames, styles }), [classNames, styles]); +// return ( +// +// +// +// +// +// ); +// }) as (( +// props: React.PropsWithChildren> & { +// ref?: React.Ref; +// }, +// ) => React.ReactElement) & { displayName?: string }; if (process.env.NODE_ENV !== 'production') { InputNumber.displayName = 'InputNumber'; diff --git a/src/StepHandler.tsx b/src/StepHandler.tsx index 05b09bf8..67d81eec 100644 --- a/src/StepHandler.tsx +++ b/src/StepHandler.tsx @@ -72,15 +72,15 @@ export default function StepHandler({ ); // ======================= Render ======================= - const handlerClassName = `${prefixCls}-handler`; + const actionClassName = `${prefixCls}-action`; const mergedClassName = clsx( - handlerClassName, - `${handlerClassName}-${action}`, + actionClassName, + `${actionClassName}-${action}`, { - [`${handlerClassName}-${action}-disabled`]: disabled, + [`${actionClassName}-${action}-disabled`]: disabled, }, - className + className, ); // fix: https://github.com/ant-design/ant-design/issues/43088 @@ -90,16 +90,12 @@ export default function StepHandler({ // So, we need to use requestAnimationFrame to ensure that the onmouseup event is executed after the onmousedown event. const safeOnStopStep = () => frameIds.current.push(raf(onStopStep)); - const sharedHandlerProps = { - unselectable: 'on' as const, - role: 'button', - onMouseUp: safeOnStopStep, - onMouseLeave: safeOnStopStep, - }; - return ( { onStepMouseDown(e); }} @@ -108,7 +104,7 @@ export default function StepHandler({ className={mergedClassName} style={style} > - {children || } + {children || } ); } diff --git a/tests/__snapshots__/baseInput.test.tsx.snap b/tests/__snapshots__/baseInput.test.tsx.snap index b7fb9cc5..aabf82e2 100644 --- a/tests/__snapshots__/baseInput.test.tsx.snap +++ b/tests/__snapshots__/baseInput.test.tsx.snap @@ -4,181 +4,130 @@ exports[`baseInput addon should render properly 1`] = `
+
-
- - Addon Before - -
-
+ + -
- - - - - - -
-
- -
-
+ +


+
-
-
- - - - - - -
-
- -
-
-
- - Addon After - -
-
-
-
-
-`; - -exports[`baseInput prefix should render properly 1`] = ` -
-
- - - Prefix - - -
-
-
+
+
+`; + +exports[`baseInput prefix should render properly 1`] = ` +
+
+ +
+ + + + - -
+
From a3abeabab5e314049b62b0401d9805ec90e6f606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 17:28:06 +0800 Subject: [PATCH 02/10] test: fix test case --- package.json | 2 +- src/InputNumber.tsx | 27 ++++++++------- src/SemanticContext.ts | 11 ------ tests/__snapshots__/baseInput.test.tsx.snap | 7 ++++ tests/click.test.tsx | 38 ++++++++++----------- tests/decimal.test.tsx | 8 ++--- tests/formatter.test.tsx | 6 ++-- tests/github.test.tsx | 6 ++-- tests/input.test.tsx | 4 +-- tests/longPress.test.tsx | 16 ++++----- tests/props.test.tsx | 32 ++++++++--------- tests/semantic.test.tsx | 20 ++++++----- 12 files changed, 89 insertions(+), 88 deletions(-) delete mode 100644 src/SemanticContext.ts diff --git a/package.json b/package.json index c18dd894..b9041d32 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/mini-decimal": "^1.0.1", - "@rc-component/util": "^1.2.0", + "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "devDependencies": { diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 28c63a77..a93c1c33 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -11,13 +11,12 @@ import proxyObject from '@rc-component/util/lib/proxyObject'; import { clsx } from 'clsx'; import * as React from 'react'; import useCursor from './hooks/useCursor'; -import SemanticContext from './SemanticContext'; import StepHandler from './StepHandler'; import { getDecupleSteps } from './utils/numberUtil'; import { BaseInputProps } from '@rc-component/input/lib/interface'; -import { InputFocusOptions } from '@rc-component/input/lib/utils/commonUtils'; import { useEvent } from '@rc-component/util'; +import { triggerFocus, type InputFocusOptions } from '@rc-component/util/lib/DOM/focus'; import useFrame from './hooks/useFrame'; export type { ValueType }; @@ -118,6 +117,8 @@ const InputNumber = React.forwardRef((props, r prefixCls = 'rc-input-number', className, style, + classNames, + styles, min, max, step = 1, @@ -150,10 +151,6 @@ const InputNumber = React.forwardRef((props, r ...inputProps } = props; - const { classNames, styles } = React.useContext(SemanticContext) || {}; - - const inputClassName = `${prefixCls}-input`; - const [focus, setFocus] = React.useState(false); const userTypingRef = React.useRef(false); @@ -166,7 +163,12 @@ const InputNumber = React.forwardRef((props, r React.useImperativeHandle(ref, () => proxyObject(inputRef.current, { - focus, + focus: (option?: InputFocusOptions) => { + triggerFocus(inputRef.current, option); + }, + blur: () => { + inputRef.current?.blur(); + }, nativeElement: rootRef.current, }), ); @@ -637,9 +639,9 @@ const InputNumber = React.forwardRef((props, r > {mode === 'spinner' && controls && downNode} - {suffix !== undefined && ( -
- {suffix} + {prefix !== undefined && ( +
+ {prefix}
)} @@ -652,7 +654,8 @@ const InputNumber = React.forwardRef((props, r step={step} {...inputProps} ref={inputRef} - className={inputClassName} + className={clsx(`${prefixCls}-input`, classNames?.input)} + style={styles?.input} value={inputValue} onChange={onInternalInput} disabled={disabled} @@ -661,7 +664,7 @@ const InputNumber = React.forwardRef((props, r {suffix !== undefined && (
- {prefix} + {suffix}
)} diff --git a/src/SemanticContext.ts b/src/SemanticContext.ts deleted file mode 100644 index d8d932a8..00000000 --- a/src/SemanticContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { InputNumberProps } from './InputNumber'; - -interface SemanticContextProps { - classNames?: InputNumberProps['classNames']; - styles?: InputNumberProps['styles']; -} - -const SemanticContext = React.createContext(undefined); - -export default SemanticContext; diff --git a/tests/__snapshots__/baseInput.test.tsx.snap b/tests/__snapshots__/baseInput.test.tsx.snap index aabf82e2..d336582c 100644 --- a/tests/__snapshots__/baseInput.test.tsx.snap +++ b/tests/__snapshots__/baseInput.test.tsx.snap @@ -94,6 +94,13 @@ exports[`baseInput prefix should render properly 1`] = `
+
+ + Prefix + +
{ } describe('basic work', () => { - testInputNumber('up button', { defaultValue: 10 }, '.rc-input-number-handler-up', 11, 'up', 'handler'); + testInputNumber('up button', { defaultValue: 10 }, '.rc-input-number-action-up', 11, 'up', 'handler'); - testInputNumber('down button', { value: 10 }, '.rc-input-number-handler-down', 9, 'down', 'handler'); + testInputNumber('down button', { value: 10 }, '.rc-input-number-action-down', 9, 'down', 'handler'); }); describe('empty input', () => { - testInputNumber('up button', {}, '.rc-input-number-handler-up', 1, 'up', 'handler'); + testInputNumber('up button', {}, '.rc-input-number-action-up', 1, 'up', 'handler'); - testInputNumber('down button', {}, '.rc-input-number-handler-down', -1, 'down', 'handler'); + testInputNumber('down button', {}, '.rc-input-number-action-down', -1, 'down', 'handler'); }); describe('empty with min & max', () => { - testInputNumber('up button', { min: 6, max: 10 }, '.rc-input-number-handler-up', 6, 'up', 'handler'); + testInputNumber('up button', { min: 6, max: 10 }, '.rc-input-number-action-up', 6, 'up', 'handler'); - testInputNumber('down button', { min: 6, max: 10 }, '.rc-input-number-handler-down', 6, 'down', 'handler'); + testInputNumber('down button', { min: 6, max: 10 }, '.rc-input-number-action-down', 6, 'down', 'handler'); }); describe('null with min & max', () => { testInputNumber( 'up button', { value: null, min: 6, max: 10 }, - '.rc-input-number-handler-up', + '.rc-input-number-action-up', 6, 'up', 'handler', @@ -73,7 +73,7 @@ describe('InputNumber.Click', () => { testInputNumber( 'down button', { value: null, min: 6, max: 10 }, - '.rc-input-number-handler-down', + '.rc-input-number-action-down', 6, 'down', 'handler', @@ -83,18 +83,18 @@ describe('InputNumber.Click', () => { describe('disabled', () => { it('none', () => { const { container } = render(); - expect(container.querySelector('.rc-input-number-handler-up-disabled')).toBeFalsy(); - expect(container.querySelector('.rc-input-number-handler-down-disabled')).toBeFalsy(); + expect(container.querySelector('.rc-input-number-action-up-disabled')).toBeFalsy(); + expect(container.querySelector('.rc-input-number-action-down-disabled')).toBeFalsy(); }); it('min', () => { const { container } = render(); - expect(container.querySelector('.rc-input-number-handler-down-disabled')).toBeTruthy(); + expect(container.querySelector('.rc-input-number-action-down-disabled')).toBeTruthy(); }); it('max', () => { const { container } = render(); - expect(container.querySelector('.rc-input-number-handler-up-disabled')).toBeTruthy(); + expect(container.querySelector('.rc-input-number-action-up-disabled')).toBeTruthy(); }); }); @@ -104,10 +104,10 @@ describe('InputNumber.Click', () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(onChange).toHaveBeenCalledWith(Number.MAX_SAFE_INTEGER); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(onChange).toHaveBeenCalledWith(Number.MAX_SAFE_INTEGER - 1); supportBigInt.mockRestore(); @@ -118,10 +118,10 @@ describe('InputNumber.Click', () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(onChange).toHaveBeenCalledWith(Number.MIN_SAFE_INTEGER); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(onChange).toHaveBeenCalledWith(Number.MIN_SAFE_INTEGER + 1); supportBigInt.mockRestore(); @@ -132,7 +132,7 @@ describe('InputNumber.Click', () => { const { container } = render( , ); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(onChange).toHaveBeenCalledWith('999999999999999983222785'); }); @@ -141,7 +141,7 @@ describe('InputNumber.Click', () => { const { container } = render( , ); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(onChange).toHaveBeenCalledWith('-10000000000000000905969665'); }); }); @@ -153,7 +153,7 @@ describe('InputNumber.Click', () => { const onBlur = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(100); }); diff --git a/tests/decimal.test.tsx b/tests/decimal.test.tsx index 68485eb7..750444f7 100644 --- a/tests/decimal.test.tsx +++ b/tests/decimal.test.tsx @@ -15,9 +15,9 @@ describe('InputNumber.Decimal', () => { it('increase and decrease decimal InputNumber by integer step', () => { const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(container.querySelector('input').value).toEqual('3.1'); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(container.querySelector('input').value).toEqual('2.1'); }); @@ -44,7 +44,7 @@ describe('InputNumber.Decimal', () => { for (let i = 0; i < 10; i += 1) { // plus until change precision - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); } fireEvent.blur(input); @@ -60,7 +60,7 @@ describe('InputNumber.Decimal', () => { const { container } = render(); expect(container.querySelector('input').value).toEqual(''); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(container.querySelector('input').value).toEqual('0.1'); }); diff --git a/tests/formatter.test.tsx b/tests/formatter.test.tsx index 1947de7d..98cdab88 100644 --- a/tests/formatter.test.tsx +++ b/tests/formatter.test.tsx @@ -15,10 +15,10 @@ describe('InputNumber.Formatter', () => { it('formatter on mousedown', () => { const { container } = render( `$ ${num}`} />); const input = container.querySelector('input'); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(input.value).toEqual('$ 6'); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(input.value).toEqual('$ 5'); }); @@ -94,7 +94,7 @@ describe('InputNumber.Formatter', () => { expect(input.value).toEqual('$ 5 boeing 737'); expect(onChange).toHaveBeenLastCalledWith(5); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'), { + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up'), { which: KeyCode.DOWN, }); expect(input.value).toEqual('$ 6 boeing 737'); diff --git a/tests/github.test.tsx b/tests/github.test.tsx index e1e3b94e..1aa3aaa7 100644 --- a/tests/github.test.tsx +++ b/tests/github.test.tsx @@ -68,7 +68,7 @@ describe('InputNumber.Github', () => { fireEvent.focus(input); fireEvent.change(input, { target: { value: 'foo' } }); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(input.value).toEqual('2'); }); @@ -121,7 +121,7 @@ describe('InputNumber.Github', () => { it('long press not trigger onChange in uncontrolled component', () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(500); @@ -519,7 +519,7 @@ describe('InputNumber.Github', () => { const { container, rerender } = render(); const input = container.querySelector('input'); // Click - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(input.value).toEqual('8'); // Keyboard change diff --git a/tests/input.test.tsx b/tests/input.test.tsx index ee0ea161..c38654e4 100644 --- a/tests/input.test.tsx +++ b/tests/input.test.tsx @@ -234,9 +234,7 @@ describe('InputNumber.Input', () => { it('wrapper', () => { const ref = React.createRef(); const { container } = render(); - expect(ref.current.nativeElement).toBe( - container.querySelector('.rc-input-number-affix-wrapper'), - ); + expect(ref.current.nativeElement).toBe(container.querySelector('.rc-input-number')); }); }); }); diff --git a/tests/longPress.test.tsx b/tests/longPress.test.tsx index ccb83e09..ace5afb1 100644 --- a/tests/longPress.test.tsx +++ b/tests/longPress.test.tsx @@ -16,7 +16,7 @@ describe('InputNumber.LongPress', () => { it('up button works', async () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(600 + 200 * 5 + 100); }); @@ -26,7 +26,7 @@ describe('InputNumber.LongPress', () => { it('down button works', async () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); act(() => { jest.advanceTimersByTime(600 + 200 * 5 + 100); @@ -37,27 +37,27 @@ describe('InputNumber.LongPress', () => { it('Simulates event calls out of order in Safari', async () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(10); }); - fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseUp(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(10); }); - fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseUp(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(10); }); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(10); }); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(10); }); - fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseUp(container.querySelector('.rc-input-number-action-up')); act(() => { jest.advanceTimersByTime(600 + 200 * 5 + 100); diff --git a/tests/props.test.tsx b/tests/props.test.tsx index 69b7b281..24d1539f 100644 --- a/tests/props.test.tsx +++ b/tests/props.test.tsx @@ -10,7 +10,7 @@ describe('InputNumber.Props', () => { const onChange = jest.fn(); const { container } = render(); for (let i = 0; i < 100; i += 1) { - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); } expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toEqual(10); @@ -23,7 +23,7 @@ describe('InputNumber.Props', () => { const onChange = jest.fn(); const { container } = render(); for (let i = 0; i < 100; i += 1) { - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); } expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toEqual(-10); @@ -35,8 +35,8 @@ describe('InputNumber.Props', () => { it('disabled', () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(container.querySelector('.rc-input-number-disabled')).toBeTruthy(); expect(onChange).not.toHaveBeenCalled(); }); @@ -44,8 +44,8 @@ describe('InputNumber.Props', () => { it('readOnly', () => { const onChange = jest.fn(); const { container } = render(); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); fireEvent.keyDown(container.querySelector('input'), { which: KeyCode.UP }); fireEvent.keyDown(container.querySelector('input'), { which: KeyCode.DOWN }); expect(container.querySelector('.rc-input-number-readonly')).toBeTruthy(); @@ -68,7 +68,7 @@ describe('InputNumber.Props', () => { const { container } = render(); for (let i = 0; i < 3; i += 1) { - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(onChange).toHaveBeenCalledWith(-5 * (i + 1)); } expect(container.querySelector('input')).toHaveAttribute('step', '5'); @@ -79,10 +79,10 @@ describe('InputNumber.Props', () => { const { container } = render(); for (let i = 0; i < 3; i += 1) { - fireEvent.keyDown(container.querySelector('.rc-input-number-handler-down'), { + fireEvent.keyDown(container.querySelector('.rc-input-number-action-down'), { shiftKey: true, }); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(onChange).toHaveBeenCalledWith(-5 * (i + 1) * 10); } @@ -100,7 +100,7 @@ describe('InputNumber.Props', () => { ); for (let i = 0; i < 11; i += 1) { - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); } expect(onChange).toHaveBeenCalledWith('-0.00000001'); @@ -118,10 +118,10 @@ describe('InputNumber.Props', () => { ); for (let i = 0; i < 11; i += 1) { - fireEvent.keyDown(container.querySelector('.rc-input-number-handler-down'), { + fireEvent.keyDown(container.querySelector('.rc-input-number-action-down'), { shiftKey: true, }); - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); } expect(onChange).toHaveBeenCalledWith('-0.00000001'); // -1e-8 @@ -133,7 +133,7 @@ describe('InputNumber.Props', () => { , ); for (let i = 0; i < 3; i += 1) { - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); } expect(onChange).toHaveBeenCalledWith(1.2); }); @@ -318,18 +318,18 @@ describe('InputNumber.Props', () => { ); const input = container.querySelector('input'); for (let i = 1; i <= 9; i += 1) { - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-up')); expect(input.value).toEqual(`0.000000${i}`); expect(onChange).toHaveBeenCalledWith(0.0000001 * i); } for (let i = 8; i >= 1; i -= 1) { - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(input.value).toEqual(`0.000000${i}`); expect(onChange).toHaveBeenCalledWith(0.0000001 * i); } - fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down')); + fireEvent.mouseDown(container.querySelector('.rc-input-number-action-down')); expect(input.value).toEqual(`0.0000000`); expect(onChange).toHaveBeenCalledWith(0); }); diff --git a/tests/semantic.test.tsx b/tests/semantic.test.tsx index d0d6f58f..092f157a 100644 --- a/tests/semantic.test.tsx +++ b/tests/semantic.test.tsx @@ -1,6 +1,5 @@ import { render } from '@testing-library/react'; import InputNumber from '../src'; -import React from 'react'; describe('InputNumber.Semantic', () => { it('support classNames and styles', () => { @@ -8,13 +7,15 @@ describe('InputNumber.Semantic', () => { prefix: 'test-prefix', input: 'test-input', suffix: 'test-suffix', - actions: 'test-handle', + actions: 'test-actions', + action: 'test-action', }; const testStyles = { prefix: { color: 'red' }, input: { color: 'blue' }, suffix: { color: 'green' }, actions: { color: 'yellow' }, + action: { color: 'pink' }, }; const { container } = render( { />, ); - const input = container.querySelector('.rc-input-number')!; + const input = container.querySelector('input')!; const prefix = container.querySelector('.rc-input-number-prefix')!; const suffix = container.querySelector('.rc-input-number-suffix')!; - const actions = container.querySelector('.rc-input-number-handler-wrap')!; - expect(input.className).toContain(testClassNames.input); - expect(prefix.className).toContain(testClassNames.prefix); - expect(suffix.className).toContain(testClassNames.suffix); - expect(actions.className).toContain(testClassNames.actions); + const actions = container.querySelector('.rc-input-number-actions')!; + const action = container.querySelector('.rc-input-number-action')!; + expect(input).toHaveClass(testClassNames.input); + expect(prefix).toHaveClass(testClassNames.prefix); + expect(suffix).toHaveClass(testClassNames.suffix); + expect(actions).toHaveClass(testClassNames.actions); + expect(action).toHaveClass(testClassNames.action); expect(prefix).toHaveStyle(testStyles.prefix); expect(input).toHaveStyle(testStyles.input); expect(suffix).toHaveStyle(testStyles.suffix); expect(actions).toHaveStyle(testStyles.actions); + expect(action).toHaveStyle(testStyles.action); }); }); From 24e2fc977f5a4addf4756d4f03ae264038b57bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 17:40:00 +0800 Subject: [PATCH 03/10] chore: fix ts def --- src/InputNumber.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index a93c1c33..9c2516b6 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -678,7 +678,11 @@ const InputNumber = React.forwardRef((props, r )}
); -}); +}) as (( + props: React.PropsWithChildren> & { + ref?: React.Ref; + }, +) => React.ReactElement) & { displayName?: string }; // const InputNumber = React.forwardRef((props, ref) => { // const { From 39ad3b3ee2071bb7a23f26bac0e9356aea4fb56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 17:43:53 +0800 Subject: [PATCH 04/10] chore: clean up --- src/InputNumber.tsx | 76 --------------------------------------------- 1 file changed, 76 deletions(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 9c2516b6..02ad8546 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -684,82 +684,6 @@ const InputNumber = React.forwardRef((props, r }, ) => React.ReactElement) & { displayName?: string }; -// const InputNumber = React.forwardRef((props, ref) => { -// const { -// mode = 'input', -// disabled, -// style, -// prefixCls = 'rc-input-number', -// value, -// prefix, -// suffix, -// addonBefore, -// addonAfter, -// className, -// classNames, -// styles, -// ...rest -// } = props; - -// const holderRef = React.useRef(null); -// const inputNumberDomRef = React.useRef(null); -// const inputFocusRef = React.useRef(null); - -// const focus = (option?: InputFocusOptions) => { -// if (inputFocusRef.current) { -// triggerFocus(inputFocusRef.current, option); -// } -// }; - -// React.useImperativeHandle(ref, () => -// proxyObject(inputFocusRef.current, { -// focus, -// nativeElement: holderRef.current.nativeElement || inputNumberDomRef.current, -// }), -// ); -// const memoizedValue = React.useMemo(() => ({ classNames, styles }), [classNames, styles]); -// return ( -// -// -// -// -// -// ); -// }) as (( -// props: React.PropsWithChildren> & { -// ref?: React.Ref; -// }, -// ) => React.ReactElement) & { displayName?: string }; - if (process.env.NODE_ENV !== 'production') { InputNumber.displayName = 'InputNumber'; } From afaef47c580315aa9a04a72225188346d405d086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 17:46:13 +0800 Subject: [PATCH 05/10] chore: use utoo --- .github/workflows/react-component-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/react-component-ci.yml b/.github/workflows/react-component-ci.yml index 5735e2d2..7b1f97cb 100644 --- a/.github/workflows/react-component-ci.yml +++ b/.github/workflows/react-component-ci.yml @@ -2,5 +2,5 @@ name: ✅ test on: [push, pull_request] jobs: test: - uses: react-component/rc-test/.github/workflows/test.yml@main + uses: react-component/rc-test/.github/workflows/test-utoo.yml@main secrets: inherit \ No newline at end of file From ac30a4e0519eb95f7a966fb097142896799c2101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 17:46:24 +0800 Subject: [PATCH 06/10] chore: clean up --- docs/demo/prefix-suffix.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/demo/prefix-suffix.tsx b/docs/demo/prefix-suffix.tsx index b41319b3..57d44c97 100644 --- a/docs/demo/prefix-suffix.tsx +++ b/docs/demo/prefix-suffix.tsx @@ -7,7 +7,7 @@ export default () => { const [value, setValue] = React.useState(100); const onChange = (val: number) => { - console.warn('onChange:', val, typeof val); + console.log('onChange:', val, typeof val); setValue(val); }; From 6d01408d662ecedb70bb58d3158bdf5e4ae98e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 17:54:45 +0800 Subject: [PATCH 07/10] chore: clean up --- src/InputNumber.tsx | 13 +++++-------- tests/baseInput.test.tsx | 26 -------------------------- tests/semantic.test.tsx | 5 +++++ 3 files changed, 10 insertions(+), 34 deletions(-) delete mode 100644 tests/baseInput.test.tsx diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 02ad8546..df677edc 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -14,7 +14,6 @@ import useCursor from './hooks/useCursor'; import StepHandler from './StepHandler'; import { getDecupleSteps } from './utils/numberUtil'; -import { BaseInputProps } from '@rc-component/input/lib/interface'; import { useEvent } from '@rc-component/util'; import { triggerFocus, type InputFocusOptions } from '@rc-component/util/lib/DOM/focus'; import useFrame from './hooks/useFrame'; @@ -51,7 +50,7 @@ const getDecimalIfValidate = (value: ValueType) => { return decimal.isInvalidate() ? null : decimal; }; -type SemanticName = 'actions' | 'input' | 'action'; +type SemanticName = 'root' | 'actions' | 'input' | 'action' | 'prefix' | 'suffix'; export interface InputNumberProps extends Omit< React.InputHTMLAttributes, @@ -75,10 +74,8 @@ export interface InputNumberProps controls?: boolean; prefix?: React.ReactNode; suffix?: React.ReactNode; - addonBefore?: React.ReactNode; - addonAfter?: React.ReactNode; - classNames?: BaseInputProps['classNames'] & Partial>; - styles?: BaseInputProps['styles'] & Partial>; + classNames?: Partial>; + styles?: Partial>; // Customize handler node upHandler?: React.ReactNode; @@ -619,14 +616,14 @@ const InputNumber = React.forwardRef((props, r return (
{ setFocus(true); }} diff --git a/tests/baseInput.test.tsx b/tests/baseInput.test.tsx deleted file mode 100644 index 0890d7be..00000000 --- a/tests/baseInput.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { render } from '@testing-library/react'; -import InputNumber from '../src'; - -describe('baseInput', () => { - it('prefix should render properly', () => { - const prefix = Prefix; - - const { container } = render(); - expect(container).toMatchSnapshot(); - }); - - it('addon should render properly', () => { - const addonBefore = Addon Before; - const addonAfter = Addon After; - - const { container } = render( -
- -
-
- -
, - ); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/tests/semantic.test.tsx b/tests/semantic.test.tsx index 092f157a..20dbaeea 100644 --- a/tests/semantic.test.tsx +++ b/tests/semantic.test.tsx @@ -4,6 +4,7 @@ import InputNumber from '../src'; describe('InputNumber.Semantic', () => { it('support classNames and styles', () => { const testClassNames = { + root: 'test-root', prefix: 'test-prefix', input: 'test-input', suffix: 'test-suffix', @@ -11,6 +12,7 @@ describe('InputNumber.Semantic', () => { action: 'test-action', }; const testStyles = { + root: { color: 'orange' }, prefix: { color: 'red' }, input: { color: 'blue' }, suffix: { color: 'green' }, @@ -27,16 +29,19 @@ describe('InputNumber.Semantic', () => { />, ); + const root = container.querySelector('.rc-input-number')!; const input = container.querySelector('input')!; const prefix = container.querySelector('.rc-input-number-prefix')!; const suffix = container.querySelector('.rc-input-number-suffix')!; const actions = container.querySelector('.rc-input-number-actions')!; const action = container.querySelector('.rc-input-number-action')!; + expect(root).toHaveClass(testClassNames.root); expect(input).toHaveClass(testClassNames.input); expect(prefix).toHaveClass(testClassNames.prefix); expect(suffix).toHaveClass(testClassNames.suffix); expect(actions).toHaveClass(testClassNames.actions); expect(action).toHaveClass(testClassNames.action); + expect(root).toHaveStyle(testStyles.root); expect(prefix).toHaveStyle(testStyles.prefix); expect(input).toHaveStyle(testStyles.input); expect(suffix).toHaveStyle(testStyles.suffix); From 5af2f920b751f70d9e1d81d583c221439026803a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 21:39:37 +0800 Subject: [PATCH 08/10] chore: rm useless deps --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index b9041d32..b6d2f9bd 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ ] }, "dependencies": { - "@rc-component/input": "~1.1.0", "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" From d6131af4a70caf0c73e683524086e8ab5375d7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 21:44:35 +0800 Subject: [PATCH 09/10] chore: fix input --- src/InputNumber.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index df677edc..353c724f 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -15,7 +15,7 @@ import StepHandler from './StepHandler'; import { getDecupleSteps } from './utils/numberUtil'; import { useEvent } from '@rc-component/util'; -import { triggerFocus, type InputFocusOptions } from '@rc-component/util/lib/DOM/focus'; +import { triggerFocus, type InputFocusOptions } from '@rc-component/util/lib/Dom/focus'; import useFrame from './hooks/useFrame'; export type { ValueType }; From 9e15c3e2bcbc488e1a626fb64380a1bc1512e747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 10 Nov 2025 21:50:10 +0800 Subject: [PATCH 10/10] test: clean up --- tests/__snapshots__/baseInput.test.tsx.snap | 141 -------------------- 1 file changed, 141 deletions(-) delete mode 100644 tests/__snapshots__/baseInput.test.tsx.snap diff --git a/tests/__snapshots__/baseInput.test.tsx.snap b/tests/__snapshots__/baseInput.test.tsx.snap deleted file mode 100644 index d336582c..00000000 --- a/tests/__snapshots__/baseInput.test.tsx.snap +++ /dev/null @@ -1,141 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`baseInput addon should render properly 1`] = ` -
-
-
- -
- - - - - - -
-
-
-
-
- -
- - - - - - -
-
-
-
-`; - -exports[`baseInput prefix should render properly 1`] = ` -
-
-
- - Prefix - -
- -
- - - - - - -
-
-
-`;