Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
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
542 changes: 542 additions & 0 deletions semcore/core/__tests__/decorators.test.ts

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions semcore/core/src/decorators/reactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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: any) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can not to use symbol here

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: any) => value !== Object(value);

function reactive<This>(cb: Callback<This>): 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);
}
},
enumerable: true,
configurable: true,
});
} else {
// @ts-ignore
this[name] = new Proxy(this[name], {
set(target, p, newValue) {
target[p] = newValue;

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

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

export default reactive;
56 changes: 56 additions & 0 deletions semcore/core/src/decorators/trackPropsChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
type WatchedProps<Props> = { [key in keyof Props]?: any };

type Constructor<Props> = new (...args: any) => {
props: Props;
onPropsChange(changedProps: WatchedProps<Props>): void;
render(): any;
};

function trackPropsChanges<
P,
C extends Constructor<P> = Constructor<P>,
>(propsToWatch: Array<keyof P>) {
const watchedProps: WatchedProps<P> = {};

return function (Class: C) {
return class extends Class {
constructor(...args: any[]) {
super(...args);

propsToWatch.reduce((acc, prop) => {
acc[prop] = this.props?.[prop];

return acc;
}, watchedProps);
}

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

propsToWatch.forEach((prop) => {
const isPropValueEqual = Object.is(watchedProps[prop], this.props[prop]);

if (!isPropValueEqual) {
watchedProps[prop] = this.props[prop];
changedProps[prop] = this.props[prop];

shouldCallFunc = true;
}
});

if (!shouldCallFunc) return;

super.onPropsChange(changedProps);
}

render() {
this.onPropsChange();

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

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

This file was deleted.

231 changes: 231 additions & 0 deletions semcore/time-picker/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
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(':', false);

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

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

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

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

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');
});

it('should format 23:59 as 11:59', () => {
const entity = new TimePickerEntity('23:59', true);

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

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

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

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

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

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

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

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

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

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

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

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

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

describe('getters and setters', () => {
it('should update hours via setter', () => {
const entity = new TimePickerEntity('10:00', false);
entity.hours = '15';

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

it('should update minutes via setter', () => {
const entity = new TimePickerEntity('10:00', false);
entity.minutes = '45';

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

it('should update meridiem via setter', () => {
const entity = new TimePickerEntity('10:00', true);
entity.meridiem = 'PM';

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

describe('toggleMeridiem', () => {
it('should toggle from AM to PM', () => {
const entity = new TimePickerEntity('10:00', true);

entity.toggleMeridiem();

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

it('should toggle from PM to AM', () => {
const entity = new TimePickerEntity('10:00', true);
entity.meridiem = 'PM';

entity.toggleMeridiem();

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

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', false);

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.meridiem = 'PM';

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

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

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

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

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

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

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

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

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

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

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

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

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');
});
});
});
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 <ui-kit-team@semrush.com>",
"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