From e1d868aae922346509485b6dd9f04fef20ae751f Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Tue, 23 Jun 2026 13:32:20 +0200 Subject: [PATCH 1/2] [UIK-5338][input-number] rewrite component to TS --- playground/entries/InputNumber.tsx | 8 +- semcore/core/src/core-types/Component.ts | 20 +- semcore/inline-input/src/InlineInput.tsx | 4 +- semcore/inline-input/src/index.type.ts | 4 +- semcore/input-number/package.json | 2 +- .../src/{InputNumber.jsx => InputNumber.tsx} | 197 ++++++++++++------ semcore/input-number/src/InputNumber.type.ts | 102 +++++++++ .../src/{buttons.jsx => buttons.tsx} | 1 + semcore/input-number/src/index.d.ts | 63 ------ .../input-number/src/{index.js => index.ts} | 2 + .../__intergalactic-dynamic-locales.ts | 2 + semcore/input-number/vite.config.ts | 2 +- semcore/pagination/src/Pagination.tsx | 1 - .../inline-input/tests/examples/styles.tsx | 4 +- .../tests/examples/basic_example.tsx | 4 +- .../tests/examples/basic_example_addon.tsx | 4 +- tsconfig.json | 2 +- .../input-number/input-number-api.md | 6 +- 18 files changed, 265 insertions(+), 163 deletions(-) rename semcore/input-number/src/{InputNumber.jsx => InputNumber.tsx} (69%) create mode 100644 semcore/input-number/src/InputNumber.type.ts rename semcore/input-number/src/{buttons.jsx => buttons.tsx} (99%) delete mode 100644 semcore/input-number/src/index.d.ts rename semcore/input-number/src/{index.js => index.ts} (76%) diff --git a/playground/entries/InputNumber.tsx b/playground/entries/InputNumber.tsx index b7904896bf..82efb1febd 100644 --- a/playground/entries/InputNumber.tsx +++ b/playground/entries/InputNumber.tsx @@ -1,4 +1,4 @@ -import type { InputNumberProps, InputNumberValueProps, InputNumberControlsProps } from '@semcore/ui/input-number'; +import type { NSInputNumber } from '@semcore/ui/input-number'; import InputNumber from '@semcore/ui/input-number'; import React from 'react'; @@ -6,7 +6,11 @@ import type { JSXProps } from '../types/JSXProps'; import type { PlaygroundEntry } from '../types/Playground'; import createGithubLink from '../utils/createGHLink'; -export type InputNumberJSXProps = JSXProps; +export type InputNumberJSXProps = JSXProps< + NSInputNumber.Props & + NSInputNumber.Value.Props & + NSInputNumber.Controls.Props +>; function getJSX(props: InputNumberJSXProps) { return ( diff --git a/semcore/core/src/core-types/Component.ts b/semcore/core/src/core-types/Component.ts index 3b074c152b..96f5f873fd 100644 --- a/semcore/core/src/core-types/Component.ts +++ b/semcore/core/src/core-types/Component.ts @@ -170,18 +170,14 @@ export namespace Intergalactic { : never; }; - type MergeChildProps = { - [K in keyof Root | keyof Component]: - K extends keyof Root - ? K extends keyof Component - ? Root[K] & Component[K] extends never - ? Root[K] | Component[K] - : Root[K] & Component[K] - : Root[K] - : K extends keyof Component - ? Component[K] - : never - }; + type MergeChildProps = + Omit & + Omit & + { + [K in keyof Root & keyof Component]: Root[K] & Component[K] extends never + ? Root[K] | Component[K] + : Root[K] & Component[K]; + }; type InferPropsFromRoot< Root extends new (...args: any) => any, diff --git a/semcore/inline-input/src/InlineInput.tsx b/semcore/inline-input/src/InlineInput.tsx index e804bb1e9d..b6fd82062e 100644 --- a/semcore/inline-input/src/InlineInput.tsx +++ b/semcore/inline-input/src/InlineInput.tsx @@ -6,7 +6,7 @@ import i18nEnhance from '@semcore/core/lib/utils/enhances/i18nEnhance'; import { hasParent } from '@semcore/core/lib/utils/hasParent'; import CheckM from '@semcore/icon/Check/m'; import CloseM from '@semcore/icon/Close/m'; -import InputNumber, { type InputNumberValueProps } from '@semcore/input-number'; +import InputNumber, { type NSInputNumber } from '@semcore/input-number'; import Spin from '@semcore/spin'; import type { TooltipProps } from '@semcore/tooltip'; import React from 'react'; @@ -69,7 +69,7 @@ type ConfirmControlAsProps = ControlAsProps & { type CancelControlAsProps = ControlAsProps & { onCancel?: OnCancel; }; -type NumberValueAsProps = InputNumberValueProps & { +type NumberValueAsProps = NSInputNumber.Value.Props & { increment?: (event: WheelEvent) => void; decrement?: (event: WheelEvent) => void; }; diff --git a/semcore/inline-input/src/index.type.ts b/semcore/inline-input/src/index.type.ts index 5e90ec862f..4b003ba24c 100644 --- a/semcore/inline-input/src/index.type.ts +++ b/semcore/inline-input/src/index.type.ts @@ -1,7 +1,7 @@ import type { BoxProps } from '@semcore/base-components'; import type { ButtonLinkComponent } from '@semcore/button'; import type { PropGetterFn, Intergalactic } from '@semcore/core'; -import type { InputNumberControlsProps } from '@semcore/input-number'; +import type { NSInputNumber } from '@semcore/input-number'; import type React from 'react'; export type InlineInputProps = BoxProps & { @@ -123,5 +123,5 @@ export type InlineInputComponent = Intergalactic.Component<'div', InlineInputPro InlineInputProps >; NumberValue: Intergalactic.Component<'div', {}, InlineInputProps>; - NumberControls: Intergalactic.Component<'div', InputNumberControlsProps, InlineInputProps>; + NumberControls: Intergalactic.Component<'div', NSInputNumber.Controls.Props, InlineInputProps>; }; diff --git a/semcore/input-number/package.json b/semcore/input-number/package.json index 5538ea6734..de0025e7cf 100644 --- a/semcore/input-number/package.json +++ b/semcore/input-number/package.json @@ -9,7 +9,7 @@ "author": "UI-kit team ", "license": "MIT", "scripts": { - "build": "pnpm semcore-builder --source=js && pnpm vite build" + "build": "pnpm semcore-builder && pnpm vite build" }, "exports": { "types": "./lib/types/index.d.ts", diff --git a/semcore/input-number/src/InputNumber.jsx b/semcore/input-number/src/InputNumber.tsx similarity index 69% rename from semcore/input-number/src/InputNumber.jsx rename to semcore/input-number/src/InputNumber.tsx index c3940a7d47..de540acb0f 100644 --- a/semcore/input-number/src/InputNumber.jsx +++ b/semcore/input-number/src/InputNumber.tsx @@ -1,40 +1,65 @@ +import type { Intergalactic } from '@semcore/core'; import { Component, createComponent, Root, sstyled } from '@semcore/core'; import { callAllEventHandlers } from '@semcore/core/lib/utils/assignProps'; +import type { WithI18nEnhanceProps } from '@semcore/core/lib/utils/enhances/i18nEnhance'; import i18nEnhance from '@semcore/core/lib/utils/enhances/i18nEnhance'; import { forkRef } from '@semcore/core/lib/utils/ref'; import Input from '@semcore/input'; import React from 'react'; import { DecrementIcon, IncrementIcon } from './buttons'; +import type { NSInputNumber } from './InputNumber.type'; import style from './style/input-number.shadow.css'; import { localizedMessages } from './translations/__intergalactic-dynamic-locales'; +/** Events that can trigger a value step (buttons, keyboard arrows, mouse wheel). */ +type StepEvent = + | React.MouseEvent + | React.KeyboardEvent + | WheelEvent; + +/** + * The input DOM node whose native `stepUp`/`stepDown` methods are replaced at runtime + * (see `Value.componentDidMount`) with event-aware versions. + */ +type InputElement = Omit & { + stepUp: (event: StepEvent) => void; + stepDown: (event: StepEvent) => void; +}; + export function parseValueWithMinMax( - value, + value: number, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER, ) { return Math.max(min, Math.min(max, value)); } -class InputNumber extends Component { +class InputNumber extends Component< + Intergalactic.InternalTypings.InferComponentProps, + typeof InputNumber.enhance, + {}, + WithI18nEnhanceProps, + {}, + NSInputNumber.DefaultProps +> { static displayName = 'InputNumber'; static style = style; - static enhance = [i18nEnhance(localizedMessages)]; + static enhance = [i18nEnhance(localizedMessages)] as const; static defaultProps = { size: 'm', i18n: localizedMessages, locale: 'en', - }; + } as const; - inputRef = React.createRef(); + inputRef = React.createRef(); - increment = (event) => { - this.inputRef.current?.stepUp?.(event); + increment = (event: React.MouseEvent) => { + (this.inputRef.current)?.stepUp?.(event); }; - decrement = (event) => { - this.inputRef.current?.stepDown?.(event); + decrement = (event: React.MouseEvent) => { + (this.inputRef.current)?.stepDown?.(event); }; getValueProps() { @@ -49,9 +74,8 @@ class InputNumber extends Component { } getControlsProps() { - const { size, getI18nText } = this.asProps; + const { getI18nText } = this.asProps; return { - size, increment: this.increment, decrement: this.decrement, getI18nText, @@ -64,33 +88,44 @@ class InputNumber extends Component { } } -class Value extends Component { +class Value extends Component< + Intergalactic.InternalTypings.InferChildComponentProps, + [], + NSInputNumber.Value.Handlers, + {}, + NSInputNumber.Value.State, + NSInputNumber.Value.DefaultProps +> { static style = style; static defaultProps = { defaultValue: '', defaultDisplayValue: '', step: 1, + } as const; + + state: NSInputNumber.Value.State = { + displayValue: '', }; - valueInputRef = React.createRef(); + valueInputRef = React.createRef(); - cursorPosition = -1; + cursorPosition: number | null = -1; - uncontrolledProps() { + uncontrolledProps(): NSInputNumber.Value.Handlers { return { - displayValue: null, value: [ null, (newValue) => { - const { value: prevValue, displayValue: prevDisplayValue } = this.asProps; + const { displayValue: prevDisplayValue } = this.state; + const { value: prevValue } = this.asProps; const { parsedValue, displayValue } = this.valueParser( - newValue, + newValue ?? '', prevValue, prevDisplayValue, ); - this.handlers.displayValue(displayValue); + this.setState({ displayValue }); return parsedValue; }, @@ -110,13 +145,17 @@ class Value extends Component { return numberFormatter.format(1111).replace(/\d/g, ''); } - getFormattedValue = (value) => { + getFormattedValue = (value: string) => { return value .replace(new RegExp(`[${this.separatorThousands}]`, 'g'), '') .replace(this.separatorDecimal, '.'); }; - valueParser = (value, prevValue, prevDisplayValue) => { + valueParser = ( + value: typeof this.asProps['value'], + prevValue: typeof this.asProps['value'], + prevDisplayValue: NSInputNumber.Value.State['displayValue'], + ) => { const { numberFormatter } = this.props; const stringNumber = this.getFormattedValue(String(value)); @@ -132,9 +171,9 @@ class Value extends Component { if (/\.[0-9]*0$/.test(stringNumber)) { const [int, decimal] = stringNumber.split(this.separatorDecimal); - displayValue = numberFormatter.format(int) + this.separatorDecimal + decimal; + displayValue = numberFormatter.format(+int) + this.separatorDecimal + decimal; } else if (stringNumber !== '') { - displayValue = numberFormatter.format(stringNumber); + displayValue = numberFormatter.format(+stringNumber); } return { @@ -143,15 +182,24 @@ class Value extends Component { }; }; - round(value, step) { - const countDecimals = Math.floor(step) === step ? 0 : step.toString().split('.')[1].length || 0; - return countDecimals === 0 - ? Number.parseFloat(value) - : Number.parseFloat(value).toPrecision(countDecimals); + getDecimalPlaces(value: number): number { + if (Number.isInteger(value)) { + return 0; + } + + const [, decimals = ''] = value.toString().split('.'); + return decimals.length; + } + + round(value: number, step: number) { + const decimals = step.toString().split('.')[1]?.length ?? 0; + + return Number(value.toFixed(decimals)); } - handleValidation = (event) => { - const { value, displayValue, min, max, step } = this.asProps; + handleValidation = (event: React.FocusEvent) => { + const { value, min, max, step } = this.asProps; + const { displayValue } = this.state; const { parsedValue } = this.valueParser(event.currentTarget.value, value, displayValue); const roundCoefficient = step < 1 ? step.toString().split('.')[1].length : 1; @@ -193,18 +241,20 @@ class Value extends Component { if (value !== '') { const { displayValue } = this.valueParser(value, '', ''); - this.handlers.displayValue(displayValue); + // this.handlers.displayValue(displayValue); + this.setState({ displayValue }); } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: typeof this.asProps, prevState: typeof this.state) { if (prevProps.value !== this.props.value) { const { displayValue } = this.valueParser( - this.props.value, + this.props.value ?? '', prevProps.value, - prevProps.displayValue, + prevState.displayValue, ); - this.handlers.displayValue(displayValue); + // this.handlers.displayValue(displayValue); + this.setState({ displayValue }); } } @@ -212,27 +262,28 @@ class Value extends Component { this.valueInputRef.current?.removeEventListener('wheel', this.onWheel); } - onWheel = (event) => { + onWheel = (event: WheelEvent) => { callAllEventHandlers(this.asProps.onWheel, this.handleWheel)(event); }; - handleWheel = (event) => { + handleWheel = (event: WheelEvent) => { if (event.target !== this.valueInputRef.current) return; if (document.activeElement !== this.valueInputRef.current) return; event.preventDefault(); - if (event.wheelDelta > 0) { + if (event.deltaY < 0) { this.stepUp(event); - } else if (event.wheelDelta < 0) { + } else if (event.deltaY > 0) { this.stepDown(event); } }; - handleChange = (event) => { + handleChange = (event: React.ChangeEvent) => { const value = this.getFormattedValue(event.currentTarget.value); const { numberFormatter, value: prevValue } = this.asProps; if (value === '.' || value === '-') { - this.handlers.displayValue(value); + this.setState({ displayValue: value }); + // this.handlers.displayValue(value); return false; } @@ -242,7 +293,8 @@ class Value extends Component { if (value.endsWith('.')) { if (value.length > prevValue.length) { - this.handlers.displayValue(numberFormatter.format(value) + this.separatorDecimal); + this.setState({ displayValue: numberFormatter.format(value as `${number}`) + this.separatorDecimal }); + // this.handlers.displayValue(numberFormatter.format(value) + this.separatorDecimal); return false; } else { this.handlers.value(value.slice(0, -1), event); @@ -257,17 +309,17 @@ class Value extends Component { } }; - handleKeyUp = (event) => { + handleKeyUp = (event: React.KeyboardEvent) => { if (event.key === 'Shift') { this.cursorPosition = -1; } }; - handleKeyDown = (event) => { + handleKeyDown = (event: React.KeyboardEvent) => { const element = event.currentTarget; const value = element.value; const length = value.length; - const { displayValue } = this.asProps; + const { displayValue } = this.state; if (event.key === '.' || event.key === ',') { // for the first decimal separator we should replace both ',' and '.' to '.' because of how js convert strings to numbers (with ',' it will be Number.NaN) @@ -283,7 +335,8 @@ class Value extends Component { if (event.key === 'Backspace' && value.endsWith(this.separatorDecimal)) { event.preventDefault(); event.stopPropagation(); - this.handlers.displayValue(displayValue.slice(0, -1)); + // this.handlers.displayValue(displayValue.slice(0, -1)); + this.setState({ displayValue: displayValue?.slice(0, -1) }); return false; } @@ -293,7 +346,7 @@ class Value extends Component { event.key === this.separatorDecimal || ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(event.key)) ) { - const afterSelection = value.slice(element.selectionEnd); + const afterSelection = value.slice(element.selectionEnd ?? undefined); requestAnimationFrame(() => { const newValue = this.state.displayValue; @@ -339,8 +392,11 @@ class Value extends Component { } }; - moveSelectionLeft = (element, cursorIndex) => { + moveSelectionLeft = (element: React.KeyboardEvent['currentTarget'], cursorIndex: number) => { + if (element.selectionStart === null) return; + const value = element.value; + const nextPosition = element.selectionStart - 1 >= 0 ? element.selectionStart - 1 : 0; const cursorPosition = @@ -353,11 +409,12 @@ class Value extends Component { element.setSelectionRange(cursorPosition, cursorPosition); } else { if ( + this.cursorPosition && element.selectionStart <= this.cursorPosition && element.selectionEnd === this.cursorPosition ) { element.setSelectionRange(cursorPosition, element.selectionEnd); - } else { + } else if (element.selectionEnd !== null) { element.setSelectionRange( element.selectionStart, value[element.selectionEnd - cursorIndex] === this.separatorThousands @@ -368,7 +425,9 @@ class Value extends Component { } }; - moveSelectionRight = (element, cursorIndex) => { + moveSelectionRight = (element: React.KeyboardEvent['currentTarget'], cursorIndex: number) => { + if (element.selectionEnd === null) return; + const value = element.value; const nextPosition = element.selectionEnd + 1; @@ -382,11 +441,12 @@ class Value extends Component { element.setSelectionRange(cursorPosition, cursorPosition); } else { if ( + this.cursorPosition && element.selectionEnd >= this.cursorPosition && element.selectionStart === this.cursorPosition ) { element.setSelectionRange(element.selectionStart, cursorPosition); - } else { + } else if (element.selectionStart !== null) { element.setSelectionRange( value[element.selectionStart] === this.separatorThousands ? element.selectionStart + cursorIndex @@ -397,21 +457,16 @@ class Value extends Component { } }; - handleClick = (event) => { - const element = event.target; + handleClick = (event: React.MouseEvent) => { + const element = event.currentTarget; const value = element.value; - if (value[element.selectionStart - 1] === this.separatorThousands) { - element.setSelectionRange(element.selectionStart - 1, element.selectionEnd - 1); + if (element.selectionStart !== null && value[element.selectionStart - 1] === this.separatorThousands) { + element.setSelectionRange(element.selectionStart - 1, element.selectionEnd! - 1); } }; - handleBlur = (event) => { - this.cursorPosition = -1; - this.handleValidation(event); - }; - - stepUp = (event) => { + stepUp = (event: StepEvent) => { const { max = Number.MAX_SAFE_INTEGER, min, step, value } = this.asProps; let numberValue; @@ -430,7 +485,7 @@ class Value extends Component { } }; - stepDown = (event) => { + stepDown = (event: StepEvent) => { const { max, min = Number.MIN_SAFE_INTEGER, step, value } = this.asProps; let numberValue; @@ -450,7 +505,8 @@ class Value extends Component { render() { const SValue = Root; - const { styles, min, max, step, forwardRef, inputRef, displayValue } = this.asProps; + const { styles, min, max, step, forwardRef, inputRef } = this.asProps; + const { displayValue } = this.state; return sstyled(styles)( <> @@ -462,7 +518,7 @@ class Value extends Component { onKeyDown={this.handleKeyDown} onKeyUp={this.handleKeyUp} onClick={this.handleClick} - use:ref={forkRef(this.valueInputRef, inputRef, forwardRef)} + use:ref={forkRef(this.valueInputRef, inputRef, forwardRef ?? null)} use:value={displayValue} inputMode={Number.isInteger(step) ? 'numeric' : 'decimal'} min={min} @@ -474,8 +530,10 @@ class Value extends Component { } } -function Controls(props) { - const { Children, increment, decrement, size, styles, getI18nText } = props; +function Controls( + props: Intergalactic.InternalTypings.InferChildComponentProps, +) { + const { Children, increment, decrement, styles, getI18nText } = props; const SControls = Root; const SUp = 'button'; const SDown = 'button'; @@ -486,7 +544,6 @@ function Controls(props) { onClick={increment} tabIndex={-1} type='button' - size={size} aria-label={getI18nText('increment')} > @@ -495,7 +552,6 @@ function Controls(props) { onClick={decrement} tabIndex={-1} type='button' - size={size} aria-label={getI18nText('decrement')} > @@ -511,7 +567,10 @@ Controls.style = style; * * {@link https://developer.semrush.com/intergalactic/components/input-number/input-number-api/|API} | {@link https://developer.semrush.com/intergalactic/components/input-number/input-number-code/|Examples} */ -export default createComponent(InputNumber, { +export default createComponent< + NSInputNumber.Component, + typeof InputNumber +>(InputNumber, { Value, Controls, Addon: Input.Addon, diff --git a/semcore/input-number/src/InputNumber.type.ts b/semcore/input-number/src/InputNumber.type.ts new file mode 100644 index 0000000000..64e9830002 --- /dev/null +++ b/semcore/input-number/src/InputNumber.type.ts @@ -0,0 +1,102 @@ +import type { Intergalactic, PropGetterFn } from '@semcore/core'; +import type { InputAddonProps, InputProps, InputValueProps } from '@semcore/input'; +import type React from 'react'; + +import type { LocalizedMessages } from './translations/__intergalactic-dynamic-locales'; + +declare namespace NSInputNumber { + type Value = string; + type Size = 'm' | 'l'; + type Props = InputProps & { + /** Input size + * @default m + * */ + size?: NSInputNumber.Size; + /** + * Locale value + */ + locale?: string; + }; + type DefaultProps = { + size: 'm'; + i18n: LocalizedMessages; + locale: 'en'; + }; + type Ctx = { + getValueProps: PropGetterFn; + getControlsProps: PropGetterFn; + getAddonProps: PropGetterFn; + }; + + namespace Value { + type Props = InputValueProps & { + /** Minimum value + * @default Number.MIN_SAFE_INTEGER + */ + min?: number; + /** Maximum value + * @default Number.MAX_SAFE_INTEGER + */ + max?: number; + /** Value change step + * @default 1 + */ + step?: number; + /** Numeric value */ + value?: NSInputNumber.Value; + /** Called when the input value changes, it returns its current value in numeric format */ + onChange?: (value: NSInputNumber.Value, event?: React.SyntheticEvent) => void; + }; + type DefaultProps = { + defaultValue: ''; + step: 1; + }; + type State = { + displayValue: NSInputNumber.Value; + }; + type Handlers = { + value: [ + null, + (value: Props['value'], event: React.SyntheticEvent | WheelEvent) => void, + ]; + }; + + type Component = Intergalactic.Component<'input', Props>; + } + + namespace Controls { + type Props = InputAddonProps & { + /** Always displays controls (steppers) + * @default false + */ + showControls?: boolean; + }; + + type Component = Intergalactic.Component<'div', Props>; + } + + namespace Addon { + type Component = Intergalactic.Component<'div', InputAddonProps>; + } + + type Component = Intergalactic.Component<'div', Props, Ctx> & { + Value: Value.Component; + Controls: Controls.Component; + Addon: Addon.Component; + }; +} + +/** @deprecated It will be removed in v18. */ +export type InputNumberValue = NSInputNumber.Value; +/** @deprecated It will be removed in v18. */ +export type InputNumberSize = NSInputNumber.Size; +/** @deprecated It will be removed in v18. */ +export type InputNumberProps = NSInputNumber.Props; +/** @deprecated It will be removed in v18. */ +export type InputNumberValueProps = NSInputNumber.Value.Props; +/** @deprecated It will be removed in v18. */ +export type InputNumberControlsProps = NSInputNumber.Controls.Props; +/** @deprecated It will be removed in v18. */ +export type InputNumberCtx = NSInputNumber.Ctx; + +export type { NSInputNumber }; diff --git a/semcore/input-number/src/buttons.jsx b/semcore/input-number/src/buttons.tsx similarity index 99% rename from semcore/input-number/src/buttons.jsx rename to semcore/input-number/src/buttons.tsx index dbd0f55f7a..c13f379dad 100644 --- a/semcore/input-number/src/buttons.jsx +++ b/semcore/input-number/src/buttons.tsx @@ -5,6 +5,7 @@ export const IncrementIcon = () => ( ); + export const DecrementIcon = () => ( diff --git a/semcore/input-number/src/index.d.ts b/semcore/input-number/src/index.d.ts deleted file mode 100644 index bcd26cdff9..0000000000 --- a/semcore/input-number/src/index.d.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Intergalactic, PropGetterFn } from '@semcore/core'; -import type { InputAddonProps, InputProps, InputValueProps } from '@semcore/input'; -import type React from 'react'; - -export type InputNumberValue = string; -export type InputNumberSize = 'm' | 'l'; - -export type InputNumberProps = InputProps & { - /** Input size - * @default m - * */ - size?: InputNumberSize; - /** - * Locale value - */ - locale?: string; -}; - -export type InputNumberValueProps = InputValueProps & { - /** Minimum value - * @default Number.MIN_SAFE_INTEGER - */ - min?: number; - /** Maximum value - * @default Number.MAX_SAFE_INTEGER - */ - max?: number; - /** Value change step - * @default 1 - */ - step?: number; - /** Numeric value */ - value?: InputNumberValue; - /** Called when the input value changes, it returns its current value in numeric format */ - onChange?: (value: InputNumberValue, event?: React.SyntheticEvent) => void; -}; - -export type InputNumberControlsProps = InputAddonProps & { - /** Always displays controls (steppers) - * @default false - */ - showControls?: boolean; -}; - -export type InputNumberCtx = { - getValueProps: PropGetterFn; - getControlsProps: PropGetterFn; - getAddonProps: PropGetterFn; -}; - -declare const InputNumber: Intergalactic.Component<'div', InputNumberProps, InputNumberCtx> & { - Value: Intergalactic.Component<'input', InputNumberValueProps>; - Controls: Intergalactic.Component<'div', InputNumberControlsProps>; - Addon: Intergalactic.Component<'div', InputAddonProps>; -}; - -export default InputNumber; - -declare const IncrementIcon: React.FC; -declare const DecrementIcon: React.FC; -declare const parseValueWithMinMax: (value: number, min?: number, max?: number) => number; - -export { IncrementIcon, DecrementIcon, parseValueWithMinMax }; diff --git a/semcore/input-number/src/index.js b/semcore/input-number/src/index.ts similarity index 76% rename from semcore/input-number/src/index.js rename to semcore/input-number/src/index.ts index de3d859a90..e4e558a565 100644 --- a/semcore/input-number/src/index.js +++ b/semcore/input-number/src/index.ts @@ -1,2 +1,4 @@ export { default, parseValueWithMinMax } from './InputNumber'; export { IncrementIcon, DecrementIcon } from './buttons'; + +export * from './InputNumber.type'; diff --git a/semcore/input-number/src/translations/__intergalactic-dynamic-locales.ts b/semcore/input-number/src/translations/__intergalactic-dynamic-locales.ts index ccae777b73..65cdb67688 100644 --- a/semcore/input-number/src/translations/__intergalactic-dynamic-locales.ts +++ b/semcore/input-number/src/translations/__intergalactic-dynamic-locales.ts @@ -29,3 +29,5 @@ export const localizedMessages = { pl, sv, }; + +export type LocalizedMessages = typeof localizedMessages; diff --git a/semcore/input-number/vite.config.ts b/semcore/input-number/vite.config.ts index 0991a16528..90f46b4cb2 100644 --- a/semcore/input-number/vite.config.ts +++ b/semcore/input-number/vite.config.ts @@ -7,7 +7,7 @@ export default mergeConfig( defineConfig({ build: { lib: { - entry: './src/index.js', + entry: './src/index.ts', }, rollupOptions: { external: ['react', 'react-dom', 'react/jsx-runtime', /@babel\/runtime\/*/, /@semcore\/*/], diff --git a/semcore/pagination/src/Pagination.tsx b/semcore/pagination/src/Pagination.tsx index 9979ba45e5..fd62ceb974 100644 --- a/semcore/pagination/src/Pagination.tsx +++ b/semcore/pagination/src/Pagination.tsx @@ -334,7 +334,6 @@ class TotalPages extends Component< // @ts-ignore diff --git a/stories/components/inline-input/tests/examples/styles.tsx b/stories/components/inline-input/tests/examples/styles.tsx index c8bde97f9b..1ec43b2fda 100644 --- a/stories/components/inline-input/tests/examples/styles.tsx +++ b/stories/components/inline-input/tests/examples/styles.tsx @@ -2,10 +2,10 @@ import SerpM from '@semcore/icon/Serp/m'; import { Flex } from '@semcore/ui/base-components'; import InlineInput from '@semcore/ui/inline-input'; import type { InlineInputProps, InlineInputValueProps } from '@semcore/ui/inline-input'; -import type { InputNumberControlsProps } from '@semcore/ui/input-number'; +import type { NSInputNumber } from '@semcore/ui/input-number'; import React from 'react'; -type ExampleInputTagsProps = InlineInputProps & InputNumberControlsProps & InlineInputValueProps; +type ExampleInputTagsProps = InlineInputProps & NSInputNumber.Controls.Props & InlineInputValueProps; const Styles = (props: ExampleInputTagsProps) => { const { disabled, loading, state, autoFocus, defaultValue, placeholder, showControls, onBlurBehavior } = props; diff --git a/stories/components/input-number/tests/examples/basic_example.tsx b/stories/components/input-number/tests/examples/basic_example.tsx index 2460addfcb..52f03ea2b6 100644 --- a/stories/components/input-number/tests/examples/basic_example.tsx +++ b/stories/components/input-number/tests/examples/basic_example.tsx @@ -1,9 +1,9 @@ import InputNumber from '@semcore/ui/input-number'; -import type { InputNumberProps, InputNumberValueProps, InputNumberControlsProps } from '@semcore/ui/input-number'; +import type { NSInputNumber } from '@semcore/ui/input-number'; import { Text } from '@semcore/ui/typography'; import React from 'react'; -type baseExampleType = InputNumberProps & InputNumberValueProps & InputNumberControlsProps & { disabledValue?: boolean }; +type baseExampleType = NSInputNumber.Props & NSInputNumber.Value.Props & NSInputNumber.Controls.Props & { disabledValue?: boolean }; const Demo = (props: baseExampleType) => { return ( <> diff --git a/stories/components/input-number/tests/examples/basic_example_addon.tsx b/stories/components/input-number/tests/examples/basic_example_addon.tsx index 099faeaf91..f7ac51f8c2 100644 --- a/stories/components/input-number/tests/examples/basic_example_addon.tsx +++ b/stories/components/input-number/tests/examples/basic_example_addon.tsx @@ -1,9 +1,9 @@ import InputNumber from '@semcore/ui/input-number'; -import type { InputNumberProps, InputNumberValueProps, InputNumberControlsProps } from '@semcore/ui/input-number'; +import type { NSInputNumber } from '@semcore/ui/input-number'; import { Text } from '@semcore/ui/typography'; import React from 'react'; -type baseExampleAddonType = InputNumberProps & InputNumberValueProps & InputNumberControlsProps & { disabledValue?: boolean }; +type baseExampleAddonType = NSInputNumber.Props & NSInputNumber.Value.Props & NSInputNumber.Controls.Props & { disabledValue?: boolean }; const Demo = (props: baseExampleAddonType) => { return ( <> diff --git a/tsconfig.json b/tsconfig.json index 94e72ec746..7b976eaa6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,7 @@ "@semcore/ui/*": ["${configDir}/semcore/*/src"], "@semcore/*": ["${configDir}/semcore/*/src"], "@tools/*": ["${configDir}/tools/*"] - } + }, }, "project": ["./semcore/*/tsconfig.json", "./tools/*/tsconfig.json"], "include": ["semcore", "tools", "stories", "playground"], diff --git a/website/docs/components/input-number/input-number-api.md b/website/docs/components/input-number/input-number-api.md index 3ad80755b1..29d185c7c8 100644 --- a/website/docs/components/input-number/input-number-api.md +++ b/website/docs/components/input-number/input-number-api.md @@ -10,7 +10,7 @@ tabs: Design('input-number'), A11y('input-number-a11y'), API('input-number-api') import InputNumber from '@semcore/ui/input-number'; ``` - + ## InputNumber.Value @@ -19,7 +19,7 @@ import InputNumber from '@semcore/ui/input-number'; ; ``` - + ## InputNumber.Controls @@ -28,7 +28,7 @@ import InputNumber from '@semcore/ui/input-number'; ; ``` - + ## InputNumber.Addon From c78e3bd70e3fdb97303d225db235bb05cedb815e Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Tue, 23 Jun 2026 16:34:16 +0200 Subject: [PATCH 2/2] [UIK-5338][input-number] rewrite component to TS --- semcore/input-number/src/InputNumber.tsx | 16 ++++------------ .../docs/style/design-tokens/design-tokens.json | 1 - 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/semcore/input-number/src/InputNumber.tsx b/semcore/input-number/src/InputNumber.tsx index de540acb0f..9dd22893e5 100644 --- a/semcore/input-number/src/InputNumber.tsx +++ b/semcore/input-number/src/InputNumber.tsx @@ -182,19 +182,11 @@ class Value extends Component< }; }; - getDecimalPlaces(value: number): number { - if (Number.isInteger(value)) { - return 0; - } - - const [, decimals = ''] = value.toString().split('.'); - return decimals.length; - } - round(value: number, step: number) { - const decimals = step.toString().split('.')[1]?.length ?? 0; - - return Number(value.toFixed(decimals)); + const countDecimals = Math.floor(step) === step ? 0 : step.toString().split('.')[1].length || 0; + return countDecimals === 0 + ? value + : Number(value.toPrecision(countDecimals)); } handleValidation = (event: React.FocusEvent) => { diff --git a/website/docs/style/design-tokens/design-tokens.json b/website/docs/style/design-tokens/design-tokens.json index 8b9ccea181..ec20b71495 100644 --- a/website/docs/style/design-tokens/design-tokens.json +++ b/website/docs/style/design-tokens/design-tokens.json @@ -418,7 +418,6 @@ "components": [ "button", "link", - "pagination", "tab-panel", "typography" ]