From a372e5d310acf5080e62ed0abf125b6606e252dd Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Fri, 8 May 2026 16:21:37 +0200 Subject: [PATCH 1/3] [UIK-5136][chore] experimental implmentation --- semcore/core/src/core-types/Component.ts | 51 ++++++++++++++++++++ semcore/core/src/index.ts | 3 +- semcore/slider/src/Slider.tsx | 59 +++++++++++++----------- semcore/slider/src/Slider.type.ts | 8 ++++ 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/semcore/core/src/core-types/Component.ts b/semcore/core/src/core-types/Component.ts index 7a0defac38..835ffcde8a 100644 --- a/semcore/core/src/core-types/Component.ts +++ b/semcore/core/src/core-types/Component.ts @@ -88,6 +88,57 @@ export abstract class Component< protected isControlled = false; } +type StripDefaultPrefix = K extends `default${infer Rest}` ? Uncapitalize : K; + +export function createTestComponent< + Props = {}, + Enhance extends readonly ((...args: any[]) => any)[] = [], + Uncontrolled extends Readonly<{ [key in keyof Props]?: UncontrolledPropValue }> = never, + InnerProps = {}, + State = {}, + DefaultProps = {}, +>() { + abstract class Component extends PureComponent { + uncontrolledProps(): [Uncontrolled] extends [never] ? never : Uncontrolled { + // @ts-ignore. This is a default value. Should be defined in related classes. + return {}; + }; + + get handlers(): Readonly<{ + [key in keyof Uncontrolled]: key extends keyof Props + ? Uncontrolled[key] extends (null | Props[key]) + ? (value: Props[key], e?: any) => void + : Uncontrolled[key] extends Array ? Uncontrolled[key][0] : Uncontrolled[key] + : never + }> { + // @ts-ignore. The body will be generated in factory + return {}; + } + + get asProps() { + return {} as Readonly< + { Root: RootResult } & BaseAsProps & + Intergalactic.InternalTypings.EfficientOmit< + AllHTMLAttributes, + keyof BaseAsProps + > & { + [DefaultPropKey in keyof DefaultProps as StripDefaultPrefix]: StripDefaultPrefix extends keyof Props + ? Exclude], undefined> + : DefaultProps[DefaultPropKey]; + } + >; + } + + Root: RootResult = undefined as any; + + isControlled = false; + + static defaultProps: (props?: Props) => DefaultProps; + } + + return Component; +} + // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Intergalactic { type ReactFCProps = C extends React.FC ? Omit : {}; diff --git a/semcore/core/src/index.ts b/semcore/core/src/index.ts index 990b261ecc..a68b7947e2 100644 --- a/semcore/core/src/index.ts +++ b/semcore/core/src/index.ts @@ -1,5 +1,5 @@ /** ============================== core ============================== */ -import { Root, Component, Intergalactic, wrapIntergalacticComponent } from './core-types/Component'; +import { Root, Component, Intergalactic, wrapIntergalacticComponent, createTestComponent } from './core-types/Component'; import type { PropGetterFn, IRootComponentProps, @@ -25,6 +25,7 @@ export { createBaseComponent, Root, Component, + createTestComponent, type UnknownProperties, Intergalactic, type PropGetterFn, diff --git a/semcore/slider/src/Slider.tsx b/semcore/slider/src/Slider.tsx index 8a051faca0..fb2dca0953 100644 --- a/semcore/slider/src/Slider.tsx +++ b/semcore/slider/src/Slider.tsx @@ -1,23 +1,24 @@ import { Flex, Box } from '@semcore/base-components'; import type { Intergalactic } from '@semcore/core'; -import { createComponent, Component, sstyled, Root } from '@semcore/core'; +import { createComponent, Component, sstyled, Root, createTestComponent } from '@semcore/core'; import reactToText from '@semcore/core/lib/utils/reactToText'; import React from 'react'; import type { NSSlider } from './Slider.type'; import style from './style/slider.shadow.css'; -const FALLBACK_MIN_VALUE = 0; -const FALLBACK_MAX_VALUE = 100; -const FALLBACK_STEP_VALUE = 1; +const FALLBACK_VALUE = 0; +// const FALLBACK_MIN_VALUE = 0; +// const FALLBACK_MAX_VALUE = 100; +// const FALLBACK_STEP_VALUE = 1; -const getNumericValue = ({ value, fallback }: { value?: NSSlider.Value; fallback: number }): number => { - if (!value) return fallback; +// const getNumericValue = ({ value, fallback }: { value?: NSSlider.Value; fallback: number }): number => { +// if (!value) return fallback; - const numericValue = Number(value); +// const numericValue = Number(value); - return isNaN(numericValue) ? fallback : numericValue; -}; +// return isNaN(numericValue) ? fallback : numericValue; +// }; const convertValueToPercent = (value: number, min: number, max: number) => { if (value > max) return 100; @@ -25,21 +26,24 @@ const convertValueToPercent = (value: number, min: number, max: number) => { return ((value - min) / (max - min)) * 100; }; -class SliderRoot extends Component< +class SliderRoot extends createTestComponent< Intergalactic.InternalTypings.InferComponentProps, [], - NSSlider.Handlers -> { + NSSlider.Handlers, + {}, + {}, + NSSlider.DefaultProps +>() { static displayName = 'Slider'; static style = style; sliderRef = React.createRef() as React.MutableRefObject; static defaultProps = () => ({ - defaultValue: FALLBACK_MIN_VALUE, - min: FALLBACK_MIN_VALUE, - max: FALLBACK_MAX_VALUE, - step: FALLBACK_STEP_VALUE, + defaultValue: 0, + min: 0, + max: 100, + step: 1, children: ( <> @@ -66,8 +70,8 @@ class SliderRoot extends Component< return { value: this.getNumericValue(), - min: getNumericValue({ value: min, fallback: FALLBACK_MIN_VALUE }), - max: getNumericValue({ value: max, fallback: FALLBACK_MAX_VALUE }), + min, + max, disabled, options, }; @@ -78,8 +82,8 @@ class SliderRoot extends Component< return { value: this.getNumericValue(), - min: getNumericValue({ value: min, fallback: FALLBACK_MIN_VALUE }), - max: getNumericValue({ value: max, fallback: FALLBACK_MAX_VALUE }), + min, + max, disabled, options, }; @@ -134,7 +138,7 @@ class SliderRoot extends Component< const lastOption = options?.[options?.length - 1]; const resolvedMax = options ? lastOption?.value : max; this.handlers.value(resolvedMax, event); - } else if (step !== undefined && max !== undefined && min !== undefined) { + } else { const relativeValue = newLeft / sliderSize; const relativeStep = step / (max - min); const countSteps = Math.round(relativeValue / relativeStep); @@ -164,7 +168,7 @@ class SliderRoot extends Component< handleSlideStep(event: React.KeyboardEvent) { event.preventDefault(); - const { min, max, step = FALLBACK_STEP_VALUE, options } = this.asProps; + const { min, max, step, options } = this.asProps; const direction = event.key === 'ArrowLeft' || event.key === 'ArrowDown' ? -1 : 1; let value = this.getNumericValue() + step * direction; @@ -182,7 +186,7 @@ class SliderRoot extends Component< event.preventDefault(); const { min, options } = this.asProps; - let value: NSSlider.Value = min ?? FALLBACK_MIN_VALUE; + let value: NSSlider.Value = min; if (options) { value = options[0].value; @@ -195,7 +199,7 @@ class SliderRoot extends Component< event.preventDefault(); const { max, options } = this.asProps; - let value: NSSlider.Value = max ?? FALLBACK_MAX_VALUE; + let value: NSSlider.Value = max; if (options) { value = options[options.length - 1].value; @@ -214,18 +218,17 @@ class SliderRoot extends Component< const numericDefault = Number(defaultValue); if (!isNaN(numericDefault)) return numericDefault; - return FALLBACK_MIN_VALUE; + return FALLBACK_VALUE; } const index = options.findIndex((option) => option.value === value); if (index === -1) { const numericDefault = Number(defaultValue); - return isNaN(numericDefault) ? FALLBACK_MIN_VALUE : numericDefault; + return isNaN(numericDefault) ? FALLBACK_VALUE : numericDefault; } - const offset = min ?? FALLBACK_MIN_VALUE; - const result = index + offset; + const result = index + min; if (min !== undefined && index < min) return min; if (max !== undefined && result > max) return max; diff --git a/semcore/slider/src/Slider.type.ts b/semcore/slider/src/Slider.type.ts index ce1a76b3a5..fac85e1db4 100644 --- a/semcore/slider/src/Slider.type.ts +++ b/semcore/slider/src/Slider.type.ts @@ -48,6 +48,14 @@ declare namespace NSSlider { options?: NSSlider.Option[]; }; + type DefaultProps = { + defaultValue: number; + min: number; + max: number; + step: number; + children: React.ReactNode; + }; + namespace Knob { type Component = typeof Box; } From 5acbaa91802b9ebcdf72b295acc7eef01b481a35 Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Mon, 11 May 2026 10:33:25 +0200 Subject: [PATCH 2/3] [UIK-5136][chore] experimental implmentation --- semcore/core/src/core-types/Component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/semcore/core/src/core-types/Component.ts b/semcore/core/src/core-types/Component.ts index 835ffcde8a..688084960d 100644 --- a/semcore/core/src/core-types/Component.ts +++ b/semcore/core/src/core-types/Component.ts @@ -96,7 +96,11 @@ export function createTestComponent< Uncontrolled extends Readonly<{ [key in keyof Props]?: UncontrolledPropValue }> = never, InnerProps = {}, State = {}, - DefaultProps = {}, + DefaultProps extends { + [K in keyof DefaultProps]: StripDefaultPrefix extends keyof Props + ? Props[StripDefaultPrefix] + : never; + } = {}, >() { abstract class Component extends PureComponent { uncontrolledProps(): [Uncontrolled] extends [never] ? never : Uncontrolled { @@ -124,7 +128,7 @@ export function createTestComponent< > & { [DefaultPropKey in keyof DefaultProps as StripDefaultPrefix]: StripDefaultPrefix extends keyof Props ? Exclude], undefined> - : DefaultProps[DefaultPropKey]; + : never; } >; } From 244cc288e8ad9275d15dbd778a3237fb4e27a68f Mon Sep 17 00:00:00 2001 From: Slizhevsky Vladislav Date: Tue, 12 May 2026 12:32:05 +0200 Subject: [PATCH 3/3] [UIK-5136][chore] experimental implmentation --- semcore/core/src/core-types/Component.ts | 69 ++++--------------- semcore/core/src/index.ts | 3 +- semcore/slider/src/Slider.tsx | 87 ++++++++++++++---------- 3 files changed, 65 insertions(+), 94 deletions(-) diff --git a/semcore/core/src/core-types/Component.ts b/semcore/core/src/core-types/Component.ts index 688084960d..e0cad8689b 100644 --- a/semcore/core/src/core-types/Component.ts +++ b/semcore/core/src/core-types/Component.ts @@ -52,12 +52,19 @@ type UncontrolledPropValue = | ((value: V, e?: any) => void | boolean | V)[] | ((e?: any) => void | boolean | V); +type StripDefaultPrefix = K extends `default${infer Rest}` ? Uncapitalize : K; + export abstract class Component< Props = {}, Enhance extends readonly ((...args: any[]) => any)[] = [], Uncontrolled extends Readonly<{ [key in keyof Props]?: UncontrolledPropValue }> = never, InnerProps = {}, State = {}, + DefaultProps extends { + [K in keyof DefaultProps]: StripDefaultPrefix extends keyof Props + ? Props[StripDefaultPrefix] + : never; + } = {}, > extends PureComponent { protected uncontrolledProps(): [Uncontrolled] extends [never] ? never : Uncontrolled { // @ts-ignore. This is a default value. Should be defined in related classes. @@ -79,7 +86,12 @@ export abstract class Component< return {} as Readonly< { Root: RootResult } & BaseAsProps & - Intergalactic.InternalTypings.EfficientOmit, keyof BaseAsProps> + Intergalactic.InternalTypings.EfficientOmit, keyof BaseAsProps> & + { + [DefaultPropKey in keyof DefaultProps as StripDefaultPrefix]: StripDefaultPrefix extends keyof Props + ? Exclude], undefined> + : never; + } >; } @@ -88,61 +100,6 @@ export abstract class Component< protected isControlled = false; } -type StripDefaultPrefix = K extends `default${infer Rest}` ? Uncapitalize : K; - -export function createTestComponent< - Props = {}, - Enhance extends readonly ((...args: any[]) => any)[] = [], - Uncontrolled extends Readonly<{ [key in keyof Props]?: UncontrolledPropValue }> = never, - InnerProps = {}, - State = {}, - DefaultProps extends { - [K in keyof DefaultProps]: StripDefaultPrefix extends keyof Props - ? Props[StripDefaultPrefix] - : never; - } = {}, ->() { - abstract class Component extends PureComponent { - uncontrolledProps(): [Uncontrolled] extends [never] ? never : Uncontrolled { - // @ts-ignore. This is a default value. Should be defined in related classes. - return {}; - }; - - get handlers(): Readonly<{ - [key in keyof Uncontrolled]: key extends keyof Props - ? Uncontrolled[key] extends (null | Props[key]) - ? (value: Props[key], e?: any) => void - : Uncontrolled[key] extends Array ? Uncontrolled[key][0] : Uncontrolled[key] - : never - }> { - // @ts-ignore. The body will be generated in factory - return {}; - } - - get asProps() { - return {} as Readonly< - { Root: RootResult } & BaseAsProps & - Intergalactic.InternalTypings.EfficientOmit< - AllHTMLAttributes, - keyof BaseAsProps - > & { - [DefaultPropKey in keyof DefaultProps as StripDefaultPrefix]: StripDefaultPrefix extends keyof Props - ? Exclude], undefined> - : never; - } - >; - } - - Root: RootResult = undefined as any; - - isControlled = false; - - static defaultProps: (props?: Props) => DefaultProps; - } - - return Component; -} - // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Intergalactic { type ReactFCProps = C extends React.FC ? Omit : {}; diff --git a/semcore/core/src/index.ts b/semcore/core/src/index.ts index a68b7947e2..990b261ecc 100644 --- a/semcore/core/src/index.ts +++ b/semcore/core/src/index.ts @@ -1,5 +1,5 @@ /** ============================== core ============================== */ -import { Root, Component, Intergalactic, wrapIntergalacticComponent, createTestComponent } from './core-types/Component'; +import { Root, Component, Intergalactic, wrapIntergalacticComponent } from './core-types/Component'; import type { PropGetterFn, IRootComponentProps, @@ -25,7 +25,6 @@ export { createBaseComponent, Root, Component, - createTestComponent, type UnknownProperties, Intergalactic, type PropGetterFn, diff --git a/semcore/slider/src/Slider.tsx b/semcore/slider/src/Slider.tsx index fb2dca0953..e5b58ba866 100644 --- a/semcore/slider/src/Slider.tsx +++ b/semcore/slider/src/Slider.tsx @@ -1,6 +1,6 @@ import { Flex, Box } from '@semcore/base-components'; import type { Intergalactic } from '@semcore/core'; -import { createComponent, Component, sstyled, Root, createTestComponent } from '@semcore/core'; +import { createComponent, Component, sstyled, Root } from '@semcore/core'; import reactToText from '@semcore/core/lib/utils/reactToText'; import React from 'react'; @@ -8,17 +8,6 @@ import type { NSSlider } from './Slider.type'; import style from './style/slider.shadow.css'; const FALLBACK_VALUE = 0; -// const FALLBACK_MIN_VALUE = 0; -// const FALLBACK_MAX_VALUE = 100; -// const FALLBACK_STEP_VALUE = 1; - -// const getNumericValue = ({ value, fallback }: { value?: NSSlider.Value; fallback: number }): number => { -// if (!value) return fallback; - -// const numericValue = Number(value); - -// return isNaN(numericValue) ? fallback : numericValue; -// }; const convertValueToPercent = (value: number, min: number, max: number) => { if (value > max) return 100; @@ -26,35 +15,61 @@ const convertValueToPercent = (value: number, min: number, max: number) => { return ((value - min) / (max - min)) * 100; }; -class SliderRoot extends createTestComponent< - Intergalactic.InternalTypings.InferComponentProps, - [], - NSSlider.Handlers, - {}, - {}, - NSSlider.DefaultProps ->() { +type StripDefaultPrefix = K extends `default${infer Rest}` ? Uncapitalize : K; +type DefaultProps = { + [K in keyof DP]: StripDefaultPrefix extends keyof P + ? P[StripDefaultPrefix] + : never +}; + +type DefaultPropsValue = [P] extends [never] + ? never + : [DP] extends [never] + ? never + : DefaultProps | ((props: P) => DefaultProps); + +export function withDefaultProps< + P = never, + DP = never, +>(value: NoInfer>) { + return function any>( + target: T, + _context: ClassDecoratorContext, + ) { + return class extends target { + static defaultProps = value; + }; + }; +} + +@withDefaultProps(() => ({ + defaultValue: 0, + min: 0, + max: 100, + step: 1, + children: ( + <> + + + + + + + ), +})) +class SliderRoot extends Component< + Intergalactic.InternalTypings.InferComponentProps, + [], + NSSlider.Handlers, + {}, + {}, + NSSlider.DefaultProps + > { static displayName = 'Slider'; static style = style; sliderRef = React.createRef() as React.MutableRefObject; - static defaultProps = () => ({ - defaultValue: 0, - min: 0, - max: 100, - step: 1, - children: ( - <> - - - - - - - ), - }); - handleRef = (node: HTMLButtonElement) => { this.sliderRef.current = node; };