Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2190694
[UIK-4606][notice-global] deprecate component (#2630)
slizhevskyv-semrush Dec 16, 2025
8fea293
[UIK-4623][data-table] fixed virtual scroll for tables with unknown r…
ilyabrower Dec 16, 2025
da43009
[carousel] fixed indicator style (#2626)
sheila-semrush Dec 17, 2025
829f8ba
UIK-4610/allure (#2639)
Valeria-Zimnitskaya Dec 17, 2025
a2ee282
[drag-and-drop] skip unstable test steps for firefox
Valeria-Zimnitskaya Dec 17, 2025
df8bbce
[drag-and-drop] fix unstable test
Valeria-Zimnitskaya Dec 17, 2025
9d403cd
[UIK-4619][chore] added translations (#2640)
ilyabrower Dec 17, 2025
37f1e68
[chore] fixed deps generator in ui package
ilyabrower Dec 17, 2025
1357916
[chore] Merge branch 'release/v16' of github.com:semrush/intergalacti…
ilyabrower Dec 17, 2025
a15eb8c
[chore] moved closeTasks and sendReleaseNotes to separate tasks
ilyabrower Dec 17, 2025
efc2f96
[chore] moved publish release notes to separate tasks
ilyabrower Dec 17, 2025
f0715f3
[UIK-3464][time-picker] rewrote component to ts/refactoring time pick…
slizhevskyv-semrush Dec 17, 2025
647997e
[UIK-4606][notice-global] deprecate component (#2630)
slizhevskyv-semrush Dec 16, 2025
21779f9
[UIK-4623][data-table] fixed virtual scroll for tables with unknown r…
ilyabrower Dec 16, 2025
6c80b3f
[carousel] fixed indicator style (#2626)
sheila-semrush Dec 17, 2025
d4babde
UIK-4610/allure (#2639)
Valeria-Zimnitskaya Dec 17, 2025
c3af396
[drag-and-drop] skip unstable test steps for firefox
Valeria-Zimnitskaya Dec 17, 2025
24d9d07
[chore] fixed deps generator in ui package
ilyabrower Dec 17, 2025
822c5fe
[drag-and-drop] fix unstable test
Valeria-Zimnitskaya Dec 17, 2025
e80a828
[UIK-4619][chore] added translations (#2640)
ilyabrower Dec 17, 2025
2fc4135
[chore] moved closeTasks and sendReleaseNotes to separate tasks
ilyabrower Dec 17, 2025
11d7394
[chore] moved publish release notes to separate tasks
ilyabrower Dec 17, 2025
199b49f
[data-table] skip test for webkit
Valeria-Zimnitskaya Dec 17, 2025
3e555a6
Merge remote-tracking branch 'origin/release/v16' into UIK-3464/time-…
slizhevskyv-semrush Dec 17, 2025
6630411
[UIK-3464][time-picker] added reactive/changeOnProps decorators and u…
slizhevskyv-semrush Dec 23, 2025
3dc1d2d
Merge remote-tracking branch 'origin/release/v16' into UIK-3464/time-…
slizhevskyv-semrush Dec 23, 2025
18b5dd4
[UIK-3464][core] uncommented test for @callOnPropsChange
slizhevskyv-semrush Dec 23, 2025
dd31537
[UIK-3464][time-picker] updated tests/small pr fixes
slizhevskyv-semrush Dec 29, 2025
633823c
[UIK-3464][time-picker] new trackPropsChanges decorator for class/uni…
slizhevskyv-semrush Dec 29, 2025
1b976c4
[UIK-3464][time-picker] updated import to @semcore/core
slizhevskyv-semrush Dec 29, 2025
63df0d2
[UIK-3464][time-picker] removed watchProps references
slizhevskyv-semrush Dec 29, 2025
ad658b0
[UIK-3464][time-picker] removed watchProps references
slizhevskyv-semrush Dec 29, 2025
442740d
[UIK-3464][time-picker] updated unit tests
slizhevskyv-semrush Jan 7, 2026
9889e54
[UIK-3464][time-picker] updated unit tests
slizhevskyv-semrush Jan 9, 2026
ba7b78f
[UIK-3464][time-picker] updated unit tests/reactive decorator logic (…
slizhevskyv-semrush Jan 9, 2026
3fb65a6
Update semcore/time-picker/src/entity/TimePickerEntity.ts
slizhevskyv-semrush Jan 12, 2026
52d9a82
[UIK-3464][time-picker] PR updates
slizhevskyv-semrush Jan 12, 2026
ceebe7d
[UIK-3464][time-picker] removed tests for getters/setters for TimePic…
slizhevskyv-semrush Jan 12, 2026
6166d2e
[UIK-3464][time-picker] PR updates
slizhevskyv-semrush Jan 12, 2026
e19f026
Merge remote-tracking branch 'origin/release/v17' into UIK-3464/time-…
slizhevskyv-semrush Jan 13, 2026
dd888b9
[UIK-3464][time-picker] type updates
slizhevskyv-semrush Jan 13, 2026
ec1a4dc
[UIK-3464][time-picker] added default value for TimePickerEntity.is12…
slizhevskyv-semrush Jan 13, 2026
379bdc7
[UIK-3464][time-picker] updated snapshots
slizhevskyv-semrush Jan 14, 2026
5e09c3d
[UIK-3464][time-picker] updated propsObserver decorator to handle pro…
slizhevskyv-semrush Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
509 changes: 509 additions & 0 deletions semcore/core/__tests__/decorators.test.ts

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions semcore/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@
"require": "./lib/utils/use/useScrollBarWidth.js",
"import": "./lib/utils/use/useScrollBarWidth.mjs",
"types": "./lib/utils/use/useScrollBarWidth.d.ts"
},
"./lib/decorators/reactive": {
"require": "./lib/decorators/reactive.js",
"import": "./lib/decorators/reactive.mjs",
"types": "./lib/decorators/reactive.d.ts"
},
"./lib/decorators/propsObserver": {
"require": "./lib/decorators/propsObserver.js",
"import": "./lib/decorators/propsObserver.mjs",
"types": "./lib/decorators/propsObserver.d.ts"
}
},
"dependencies": {
Expand Down
67 changes: 67 additions & 0 deletions semcore/core/src/decorators/propsObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
type WatchedProps<Props> = { [key in keyof Props]?: unknown };

type Constructor<Props> = new (...args: any[]) => {
props: Props;
onPropsChange(changedProps: WatchedProps<Props>): void;
render(): React.ReactNode;
componentWillUnmount?(): void;
};

function propsObserver<
P extends Record<string, any>,
C extends Constructor<P>,
>(propsToWatch: Array<keyof P>) {
return function (Class: C): C & Constructor<P> {
return class extends Class {
__observableProps: WatchedProps<P> = {};

constructor(...args: any[]) {
super(...args);

if (!this.props) return;

const observablePropKeys = propsToWatch.length === 0 ? Object.keys(this.props) : [...propsToWatch];

observablePropKeys.forEach((key) => {
this.__observableProps[key] = this.props[key];
});
}

onPropsChange(_?: WatchedProps<P>) {
let shouldEmitChanges = false;
const changedProps: WatchedProps<P> = {};

Object.entries(this.__observableProps).forEach(([key, value]: [key: keyof P, value: unknown]) => {
const arePropsEqual = Object.is(value, this.props[key]);

if (!arePropsEqual) {
this.__observableProps[key] = this.props[key];
changedProps[key] = this.props[key];

shouldEmitChanges = true;
}
});

if (!shouldEmitChanges) return;

super.onPropsChange(changedProps);
}

componentWillUnmount() {
super.componentWillUnmount?.();

Object.keys(this.__observableProps).forEach((key: keyof P) => {
this.__observableProps[key] = undefined;
});
}

render() {
this.onPropsChange();

return super.render();
}
};
};
}

export default propsObserver;
82 changes: 82 additions & 0 deletions semcore/core/src/decorators/reactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
type Primitive = string | number | boolean | symbol | bigint | null | undefined;
type IsReadonly<This, Property extends keyof This> =
(<F>() => F extends { [P in Property]: This[Property] } ? 1 : 2) extends
(<F>() => F extends { -readonly [P in Property]: This[Property] } ? 1 : 2)
? false
: true;
type Callback<This> = (this: This, field: string | symbol, newValue: unknown) => void;
type ReturnType<
This,
Property extends keyof This = keyof This,
> = IsReadonly<This, Property> extends true
? (_: undefined, ctx: ClassFieldDecoratorContext<This, This[Property]>) => void
: This[Property] extends Primitive
? (_: undefined, ctx: ClassFieldDecoratorContext<This, This[Property]>) => void
: never;

const isPrimitiveValue = (value: unknown) => value !== Object(value);

function reactive<
This,
Property extends keyof This,
Value = This[Property],
>(cb: Value extends Primitive ? Callback<This> : never): ReturnType<This>;
function reactive<
This,
Property extends keyof This,
Value = This[Property],
>(watchedFields: Value extends Primitive ? never : Array<keyof Value>, cb: Callback<This>): ReturnType<This>;

function reactive<
This,
Property extends keyof This,
>(watchedFieldsOrCb: Array<keyof This[Property]> | Callback<This>, cb?: Callback<This>) {
return function (_: undefined, ctx: ClassFieldDecoratorContext<This, This[Property]>) {
const { addInitializer, name } = ctx;

addInitializer(function (this: This) {
const thisRoot = this;

const isPrimitive = isPrimitiveValue(this[name as Property]);

const callback = typeof watchedFieldsOrCb === 'function' ? watchedFieldsOrCb : cb!;
const fields = Array.isArray(watchedFieldsOrCb) ? watchedFieldsOrCb : null;

if (isPrimitive) {
let value = this[name as Property];

Object.defineProperty(this, name, {
get() {
return value;
},
set(newValue) {
const oldValue = value;

value = newValue;

if (oldValue !== newValue) {
callback.call(thisRoot, name, newValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to talk about thisRoot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed

}
},
enumerable: true,
configurable: true,
});
} else {
// @ts-ignore
this[name] = new Proxy(this[name], {
set(target, p, newValue) {
target[p] = newValue;

if (fields?.length === 0 || fields?.includes(p as keyof This[Property])) {
callback.call(thisRoot, p, newValue);
}

return true;
},
});
}
});
};
}

export default reactive;
6 changes: 0 additions & 6 deletions semcore/time-picker/__tests__/index.test.jsx

This file was deleted.

181 changes: 181 additions & 0 deletions semcore/time-picker/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { runDependencyCheckTests } from '@semcore/testing-utils/shared-tests';
import { describe, it, expect } from '@semcore/testing-utils/vitest';

import TimePickerEntity from '../src/entity/TimePickerEntity';

describe('time-picker Dependency imports', () => {
runDependencyCheckTests('time-picker');
});

describe('TimePickerEntity', () => {
describe('constructor', () => {
it('should initialize with default empty time when no value provided', () => {
const entity = new TimePickerEntity(':');

expect(entity.hours).toBe('');
expect(entity.minutes).toBe('');
});

it('should parse hours and minutes from value string', () => {
const entity = new TimePickerEntity('14:30');

expect(entity.hours).toBe('14');
expect(entity.minutes).toBe('30');
});

it('should handle single digit hours and minutes', () => {
const entity = new TimePickerEntity('9:5');

expect(entity.hours).toBe('09');
expect(entity.minutes).toBe('05');
});

it('should initialize with AM meridiem by default', () => {
const entity = new TimePickerEntity('10:00', true);

expect(entity.meridiem).toBe('AM');
});
});

describe('12-hour format', () => {
it('should format midnight (00:00) as 12:00 AM', () => {
const entity = new TimePickerEntity('0:00', true);

expect(entity.hours).toBe('12');
expect(entity.minutes).toBe('00');
});

it('should format hours 1-11 AM correctly', () => {
const entity = new TimePickerEntity('9:30', true);

expect(entity.hours).toBe('09');
});

it('should format noon (12:00) as 12:00', () => {
const entity = new TimePickerEntity('12:00', true);

expect(entity.hours).toBe('12');
});

it('should format hours 13-23 as 1-11 PM', () => {
const entity = new TimePickerEntity('14:45', true);

expect(entity.hours).toBe('02');
});
});

describe('24-hour format', () => {
it('should format hours with leading zero in 24-hour mode', () => {
const entity = new TimePickerEntity('9:30');

expect(entity.hours).toBe('09');
});

it('should handle midnight in 24-hour format', () => {
const entity = new TimePickerEntity('0:00');

expect(entity.hours).toBe('00');
});

it('should convert 12 AM to 00:00 in 24-hour format', () => {
const entity = new TimePickerEntity('12:00');

expect(entity.hours).toBe('00');
});

it('should convert 12 PM to 12:00 in 24-hour format', () => {
const entity = new TimePickerEntity('12:00');
entity.toggleMeridiem();

expect(entity.hours).toBe('12');
});

it('should convert PM hours correctly', () => {
const entity = new TimePickerEntity('3:00');
entity.toggleMeridiem();

expect(entity.hours).toBe('15');
});

it('should keep AM hours unchanged (except 12)', () => {
const entity = new TimePickerEntity('9:00');

expect(entity.hours).toBe('09');
});
});

describe('toggleMeridiem', () => {
it('should toggle multiple times correctly', () => {
const entity = new TimePickerEntity('10:00', true);

entity.toggleMeridiem();
expect(entity.meridiem).toBe('PM');

entity.toggleMeridiem();
expect(entity.meridiem).toBe('AM');

entity.toggleMeridiem();
expect(entity.meridiem).toBe('PM');
});
});

describe('toString', () => {
it('should return 24-hour format string when is12Hour is false', () => {
const entity = new TimePickerEntity('14:30');

expect(entity.toString()).toBe('14:30');
});

it('should convert to 24-hour format string when is12Hour is true', () => {
const entity = new TimePickerEntity('2:30', true);
entity.toggleMeridiem();

expect(entity.toString()).toBe('14:30');
});

it('should handle midnight (12 AM) conversion', () => {
const entity = new TimePickerEntity('12:00', true);

expect(entity.toString()).toBe('00:00');
});

it('should handle noon (12 PM) conversion', () => {
const entity = new TimePickerEntity('12:00', true);
entity.toggleMeridiem();

expect(entity.toString()).toBe('12:00');
});

it('should add leading zeros to output', () => {
const entity = new TimePickerEntity('9:5');

expect(entity.toString()).toBe('09:05');
});
});

describe('edge cases', () => {
it('should handle invalid hours gracefully', () => {
const entity = new TimePickerEntity('invalid:30');

expect(entity.hours).toBe('invalid');
});

it('should handle empty minutes', () => {
const entity = new TimePickerEntity('10:');

expect(entity.minutes).toBe('');
});

it('should handle empty hours', () => {
const entity = new TimePickerEntity(':30');

expect(entity.hours).toBe('');
});

it('should preserve non-numeric hour values in 12-hour format', () => {
const entity = new TimePickerEntity('invalid:30', true);

expect(entity.hours).toBe('invalid');
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion semcore/time-picker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"author": "UI-kit team <[email protected]>",
"license": "MIT",
"scripts": {
"build": "pnpm semcore-builder --source=js && pnpm vite build"
"build": "pnpm semcore-builder && pnpm vite build"
},
"exports": {
"require": "./lib/cjs/index.js",
Expand Down
Loading
Loading