From af961b84f8765e7d2147c80210e3a8ac6ed30597 Mon Sep 17 00:00:00 2001
From: Stanislav Zaycev <102649815+KrollikRoddzer@users.noreply.github.com>
Date: Mon, 20 May 2024 11:36:34 +0300
Subject: [PATCH] feat(kit): `Time` & `DateTime` support increment / decrement
of time segment via `ArrowUp` / `ArrowDown` (#1223)
---
.../kit/date-time/date-time-time-step.cy.ts | 150 ++++++++++
.../src/tests/kit/time/time-step.cy.ts | 258 ++++++++++++++++++
.../date-time/date-time-mask-doc.component.ts | 9 +
.../date-time-mask-doc.template.html | 42 +++
.../examples/4-time-step/component.ts | 39 +++
.../date-time/examples/4-time-step/mask.ts | 7 +
.../kit/time/examples/3-step/component.ts | 38 +++
.../pages/kit/time/examples/3-step/mask.ts | 6 +
.../pages/kit/time/time-mask-doc.component.ts | 8 +
.../kit/time/time-mask-doc.template.html | 41 +++
.../src/lib/masks/date-time/date-time-mask.ts | 12 +-
projects/kit/src/lib/masks/time/time-mask.ts | 10 +
projects/kit/src/lib/plugins/index.ts | 1 +
.../src/lib/plugins/time-segments-stepping.ts | 120 ++++++++
14 files changed, 740 insertions(+), 1 deletion(-)
create mode 100644 projects/demo-integrations/src/tests/kit/date-time/date-time-time-step.cy.ts
create mode 100644 projects/demo-integrations/src/tests/kit/time/time-step.cy.ts
create mode 100644 projects/demo/src/pages/kit/date-time/examples/4-time-step/component.ts
create mode 100644 projects/demo/src/pages/kit/date-time/examples/4-time-step/mask.ts
create mode 100644 projects/demo/src/pages/kit/time/examples/3-step/component.ts
create mode 100644 projects/demo/src/pages/kit/time/examples/3-step/mask.ts
create mode 100644 projects/kit/src/lib/plugins/time-segments-stepping.ts
diff --git a/projects/demo-integrations/src/tests/kit/date-time/date-time-time-step.cy.ts b/projects/demo-integrations/src/tests/kit/date-time/date-time-time-step.cy.ts
new file mode 100644
index 000000000..6bff0b062
--- /dev/null
+++ b/projects/demo-integrations/src/tests/kit/date-time/date-time-time-step.cy.ts
@@ -0,0 +1,150 @@
+import {DemoPath} from '@demo/constants';
+import {BROWSER_SUPPORTS_REAL_EVENTS} from 'projects/demo-integrations/src/support/constants';
+
+describe('DateTime | timeStep', () => {
+ describe('yy/mm;HH:MM:SS.MSS', () => {
+ describe('timeStep = 1, initial state = 22.12;', () => {
+ beforeEach(() => {
+ cy.visit(
+ `/${DemoPath.DateTime}/API?dateTimeSeparator=;&dateMode=yy%2Fmm&timeStep=1&timeMode=HH:MM:SS.MSS`,
+ );
+ cy.get('#demo-content input')
+ .should('be.visible')
+ .first()
+ .focus()
+ .as('input');
+
+ cy.get('@input')
+ .type('2212.')
+ .should('have.value', '22.12;')
+ .should('have.a.prop', 'selectionStart', '22.12;'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;'.length);
+ });
+
+ it('decrements hours segment by pressing ArrowDown at different places of the segment', () => {
+ cy.get('@input')
+ .type('{downArrow}')
+ .should('have.value', '22.12;23')
+ .should('have.a.prop', 'selectionStart', '22.12;'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;'.length)
+ .type('{rightArrow}{downArrow}')
+ .should('have.value', '22.12;22')
+ .should('have.a.prop', 'selectionStart', '22.12;2'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;2'.length)
+ .type('{rightArrow}')
+ .type('{upArrow}'.repeat(12))
+ .should('have.value', '22.12;10')
+ .should('have.a.prop', 'selectionStart', '22.12;10'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;10'.length);
+ });
+
+ it('increments/decrements minutes segment by pressing keyboard arrows at different places of the segment', () => {
+ cy.get('@input')
+ .type('12:')
+ .should('have.value', '22.12;12:')
+ .should('have.a.prop', 'selectionStart', '22.12;12:'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:'.length)
+ .type('{upArrow}')
+ .should('have.value', '22.12;12:01')
+ .should('have.a.prop', 'selectionStart', '22.12;12:'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:'.length)
+ .type('{rightArrow}')
+ .type('{upArrow}'.repeat(9))
+ .should('have.value', '22.12;12:10')
+ .should('have.a.prop', 'selectionStart', '22.12;12:1'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:1'.length)
+ .type('{rightArrow}')
+ .type('{downArrow}'.repeat(34))
+ .should('have.value', '22.12;12:36')
+ .should('have.a.prop', 'selectionStart', '22.12;12:36'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:36'.length);
+ });
+
+ it('changes seconds segment by pressing keyboard arrow up/down', () => {
+ cy.get('@input')
+ .type('12:10:')
+ .should('have.value', '22.12;12:10:')
+ .should('have.a.prop', 'selectionStart', '22.12;12:10:'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:10:'.length)
+ .type('{downArrow}'.repeat(6))
+ .should('have.value', '22.12;12:10:54')
+ .should('have.a.prop', 'selectionStart', '22.12;12:10:'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:10:'.length)
+ .type('{rightArrow}{upArrow}'.repeat(2))
+ .should('have.value', '22.12;12:10:56')
+ .should('have.a.prop', 'selectionStart', '22.12;12:10:56'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:10:56'.length);
+ });
+
+ it('changes milliseconds segment by pressing keyboard arrow up/down', () => {
+ cy.get('@input')
+ .type('213212.')
+ .should('have.value', '22.12;21:32:12.')
+ .should('have.a.prop', 'selectionStart', '22.12;21:32:12.'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;21:32:12.'.length)
+ .type('{upArrow}{rightArrow}'.repeat(3))
+ .type('{downArrow}')
+ .should('have.value', '22.12;21:32:12.002')
+ .should('have.a.prop', 'selectionStart', '22.12;21:32:12.002'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;21:32:12.002'.length);
+ });
+
+ it('type 213212. => 22.12;21:32:12.| => type ({downArrow}{rightArrow}) * 3 + {downArrow} => 22.12:21:32:12.995|', () => {
+ cy.get('@input')
+ .type('213212.')
+ .should('have.value', '22.12;21:32:12.')
+ .should('have.a.prop', 'selectionStart', '22.12;21:32:12.'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;21:32:12.'.length)
+ .type('{downArrow}{rightArrow}'.repeat(3))
+ .type('{downArrow}')
+ .should('have.value', '22.12;21:32:12.996')
+ .should('have.a.prop', 'selectionStart', '22.12;21:32:12.996'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;21:32:12.996'.length);
+ });
+
+ it('should affect only time segments', () => {
+ cy.get('@input')
+ .type('123456111')
+ .should('have.value', '22.12;12:34:56.111')
+ .should('have.a.prop', 'selectionStart', '22.12;12:34:56.111'.length)
+ .should('have.a.prop', 'selectionEnd', '22.12;12:34:56.111'.length)
+ .type('{upArrow}{leftArrow}'.repeat('22.12;12:34:56.111'.length))
+ .should('have.value', '22.12;15:37:59.115')
+ .should('have.a.prop', 'selectionStart', 0)
+ .should('have.a.prop', 'selectionEnd', 0);
+ });
+ });
+
+ describe('timeStep = 0 (disabled time stepping)', () => {
+ beforeEach(() => {
+ cy.visit(
+ `/${DemoPath.DateTime}/API?dateTimeSeparator=;&dateMode=yy%2Fmm&timeStep=0&timeMode=HH:MM:SS.MSS`,
+ );
+ cy.get('#demo-content input')
+ .should('be.visible')
+ .first()
+ .focus()
+ .as('input');
+
+ cy.get('@input')
+ .type('1202123456000')
+ .should('have.value', '12.02;12:34:56.000')
+ .should('have.a.prop', 'selectionStart', '12.02;12:34:56.000'.length)
+ .should('have.a.prop', 'selectionEnd', '12.02;12:34:56.000'.length);
+ });
+
+ it('should be disabled', BROWSER_SUPPORTS_REAL_EVENTS, () => {
+ cy.get('@input').realPress('ArrowUp');
+
+ cy.get('@input')
+ .should('have.a.prop', 'selectionStart', 0)
+ .should('have.a.prop', 'selectionEnd', 0)
+ .realPress('ArrowDown');
+
+ cy.get('@input')
+ .should('have.a.prop', 'selectionStart', '12.02;12:34:56.000'.length)
+ .should('have.a.prop', 'selectionEnd', '12.02;12:34:56.000'.length);
+ });
+ });
+ });
+});
diff --git a/projects/demo-integrations/src/tests/kit/time/time-step.cy.ts b/projects/demo-integrations/src/tests/kit/time/time-step.cy.ts
new file mode 100644
index 000000000..c2d236f1d
--- /dev/null
+++ b/projects/demo-integrations/src/tests/kit/time/time-step.cy.ts
@@ -0,0 +1,258 @@
+import {DemoPath} from '@demo/constants';
+import {BROWSER_SUPPORTS_REAL_EVENTS} from 'projects/demo-integrations/src/support/constants';
+
+describe('Time', () => {
+ describe('Mode', () => {
+ describe('HH:MM', () => {
+ describe('step = 1', () => {
+ beforeEach(() => {
+ cy.visit(`/${DemoPath.Time}/API?mode=HH:MM&step=1`);
+ cy.get('#demo-content input')
+ .should('be.visible')
+ .first()
+ .focus()
+ .clear()
+ .as('input');
+ });
+
+ it('empty field => {upArrow} => 01', () => {
+ cy.get('@input').type('{upArrow}').should('have.value', '01');
+ });
+
+ it('type 2 + type {upArrow} => 21', () => {
+ cy.get('@input').type('2{upArrow}').should('have.value', '21');
+ });
+
+ it('{downArrow} => |23 => 23| => type {upArrow} => 23:01', () => {
+ cy.get('@input')
+ .type('{downArrow}')
+ .should('have.value', '23')
+ .should('have.a.prop', 'selectionStart', 0)
+ .should('have.a.prop', 'selectionEnd', 0)
+ .type('{rightArrow}'.repeat(2))
+ .type(':{upArrow}')
+ .should('have.value', '23:01');
+ });
+
+ it('{downArrow} => |23 => 23| => type :{downArrow} => 23:59', () => {
+ cy.get('@input')
+ .type('{downArrow}')
+ .should('have.value', '23')
+ .should('have.a.prop', 'selectionStart', 0)
+ .should('have.a.prop', 'selectionEnd', 0)
+ .type('{rightArrow}'.repeat(2))
+ .type(':{downArrow}')
+ .should('have.value', '23:59');
+ });
+
+ it('{upArrow}*24 => 00', () => {
+ cy.get('@input')
+ .type('{upArrow}'.repeat(24))
+ .should('have.value', '00');
+ });
+
+ it('type {upArrow}*12 => |12 => 1|2 => type {upArrow} => 1|3 => 13| => type {downArrow} => 12|', () => {
+ cy.get('@input')
+ .type('{upArrow}'.repeat(12))
+ .should('have.value', '12')
+ .should('have.a.prop', 'selectionStart', 0)
+ .should('have.a.prop', 'selectionEnd', 0)
+ .type('{rightArrow}')
+ .should('have.a.prop', 'selectionStart', 1)
+ .should('have.a.prop', 'selectionStart', 1)
+ .type('{upArrow}')
+ .should('have.value', '13')
+ .should('have.a.prop', 'selectionStart', 1)
+ .should('have.a.prop', 'selectionStart', 1)
+ .type('{rightArrow}')
+ .should('have.a.prop', 'selectionStart', 2)
+ .should('have.a.prop', 'selectionStart', 2)
+ .type('{downArrow}')
+ .should('have.value', '12')
+ .should('have.a.prop', 'selectionStart', 2)
+ .should('have.a.prop', 'selectionStart', 2);
+ });
+
+ it('type 12:{upArrow}{rightArrow}{downArrow}{rightArrow}{downArrow} => 12:59', () => {
+ cy.get('@input')
+ .type('12:{upArrow}')
+ .type('{rightArrow}{downArrow}'.repeat(2))
+ .should('have.value', '12:59');
+ });
+ });
+
+ describe('step = 0', () => {
+ beforeEach(() => {
+ cy.visit(`/${DemoPath.Time}/API?mode=HH:MM&step=0`);
+ cy.get('#demo-content input')
+ .should('be.visible')
+ .first()
+ .focus()
+ .clear()
+ .as('input');
+ });
+
+ it('should be disabled', BROWSER_SUPPORTS_REAL_EVENTS, () => {
+ cy.get('@input').type('1212').realPress('ArrowUp');
+
+ cy.get('@input')
+ .should('have.a.prop', 'selectionStart', 0)
+ .should('have.a.prop', 'selectionEnd', 0)
+ .realPress('ArrowDown');
+
+ cy.get('@input')
+ .should('have.a.prop', 'selectionStart', '12:12'.length)
+ .should('have.a.prop', 'selectionEnd', '12:12'.length);
+ });
+ });
+ });
+
+ describe('HH:MM:SS.MSS', () => {
+ describe('step = 1', () => {
+ beforeEach(() => {
+ cy.visit(`/${DemoPath.Time}/API?mode=HH:MM:SS.MSS&step=1`);
+ cy.get('#demo-content input')
+ .should('be.visible')
+ .first()
+ .focus()
+ .clear()
+ .as('input');
+ });
+
+ it('correctly works for hour segments', () => {
+ cy.get('@input')
+ .type('{upArrow}')
+ .should('have.value', '01')
+ .type('{rightArrow}{upArrow}')
+ .should('have.value', '02')
+ .should('have.a.prop', 'selectionStart', 1)
+ .should('have.a.prop', 'selectionEnd', 1)
+ .type('{rightArrow}')
+ .type('{downArrow}'.repeat(3))
+ .should('have.value', '23')
+ .should('have.a.prop', 'selectionStart', 2)
+ .should('have.a.prop', 'selectionEnd', 2);
+ });
+
+ it('correctly works for minute segments', () => {
+ cy.get('@input')
+ .type('12:')
+ .should('have.value', '12:')
+ .should('have.a.prop', 'selectionStart', '12:'.length)
+ .should('have.a.prop', 'selectionEnd', '12:'.length)
+ .type('{upArrow}')
+ .should('have.a.prop', 'selectionStart', '12:'.length)
+ .should('have.a.prop', 'selectionEnd', '12:'.length)
+ .should('have.value', '12:01')
+ .type('{rightArrow}')
+ .type('{downArrow}'.repeat(2))
+ .should('have.a.prop', 'selectionStart', '12:5'.length)
+ .should('have.a.prop', 'selectionEnd', '12:5'.length)
+ .should('have.value', '12:59')
+ .type('{rightArrow}')
+ .type('{downArrow}'.repeat(4))
+ .should('have.a.prop', 'selectionStart', '12:55'.length)
+ .should('have.a.prop', 'selectionEnd', '12:55'.length)
+ .should('have.value', '12:55');
+ });
+
+ it('correctly works for second segments', () => {
+ cy.get('@input')
+ .type('1234:')
+ .should('have.value', '12:34:')
+ .should('have.a.prop', 'selectionStart', '12:34:'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:'.length)
+ .type('{upArrow}'.repeat(5))
+ .should('have.a.prop', 'selectionStart', '12:34:'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:'.length)
+ .should('have.value', '12:34:05')
+ .type('{rightArrow}')
+ .type('{downArrow}'.repeat(8))
+ .should('have.a.prop', 'selectionStart', '12:34:5'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:5'.length)
+ .should('have.value', '12:34:57')
+ .type('{rightArrow}')
+ .type('{upArrow}'.repeat(3))
+ .should('have.a.prop', 'selectionStart', '12:34:00'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:00'.length)
+ .should('have.value', '12:34:00');
+ });
+
+ it('correctly works for millisecond segments', () => {
+ cy.get('@input')
+ .type('123456.')
+ .should('have.value', '12:34:56.')
+ .should('have.a.prop', 'selectionStart', '12:34:56.'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:56.'.length)
+ .type('{upArrow}'.repeat(23))
+ .should('have.value', '12:34:56.023')
+ .should('have.a.prop', 'selectionStart', '12:34:56.'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:56.'.length)
+ .type('{rightArrow}{downArrow}')
+ .should('have.value', '12:34:56.022')
+ .should('have.a.prop', 'selectionStart', '12:34:56.0'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:56.0'.length)
+ .type('{rightArrow}')
+ .type('{upArrow}'.repeat(3))
+ .should('have.value', '12:34:56.025')
+ .should('have.a.prop', 'selectionStart', '12:34:56.02'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:56.02'.length)
+ .type('{rightArrow}')
+ .type('{downArrow}'.repeat(29))
+ .should('have.value', '12:34:56.996')
+ .should('have.a.prop', 'selectionStart', '12:34:56.996'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:56.996'.length);
+ });
+ });
+
+ describe('step = 10', () => {
+ beforeEach(() => {
+ cy.visit(`/${DemoPath.Time}/API?mode=HH:MM:SS.MSS&step=10`);
+ cy.get('#demo-content input')
+ .should('be.visible')
+ .first()
+ .focus()
+ .clear()
+ .as('input');
+ });
+
+ it('correctly works for millisecond segments', () => {
+ cy.get('@input')
+ .type('123456.')
+ .should('have.value', '12:34:56.')
+ .type('{upArrow}'.repeat(20))
+ .should('have.value', '12:34:56.200')
+ .type('{downArrow}'.repeat(30))
+ .should('have.value', '12:34:56.900');
+ });
+
+ it('correctly works for each time segment', () => {
+ cy.get('@input')
+ .type('123456000')
+ .should('have.value', '12:34:56.000')
+ .should('have.a.prop', 'selectionStart', '12:34:56.000'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:56.000'.length)
+ .type('{downArrow}')
+ .should('have.value', '12:34:56.990')
+ .should('have.a.prop', 'selectionStart', '12:34:56.990'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:56.990'.length)
+ .type('{leftArrow}'.repeat(4))
+ .type('{downArrow}')
+ .should('have.value', '12:34:46.990')
+ .should('have.a.prop', 'selectionStart', '12:34:46'.length)
+ .should('have.a.prop', 'selectionEnd', '12:34:46'.length)
+ .type('{leftArrow}'.repeat(3))
+ .type('{upArrow}')
+ .should('have.value', '12:44:46.990')
+ .should('have.a.prop', 'selectionStart', '12:44'.length)
+ .should('have.a.prop', 'selectionEnd', '12:44'.length)
+ .type('{leftArrow}'.repeat(3))
+ .type('{upArrow}')
+ .should('have.value', '22:44:46.990')
+ .should('have.a.prop', 'selectionStart', '22'.length)
+ .should('have.a.prop', 'selectionEnd', '22'.length);
+ });
+ });
+ });
+ });
+});
diff --git a/projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts
index ead832f3a..8c7c0cfe3 100644
--- a/projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts
+++ b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts
@@ -14,6 +14,7 @@ import {DATE_TIME_SEPARATOR, TuiInputModule} from '@taiga-ui/kit';
import {DateTimeMaskDocExample1} from './examples/1-date-time-localization/component';
import {DateTimeMaskDocExample2} from './examples/2-date-time-separator/component';
import {DateTimeMaskDocExample3} from './examples/3-min-max/component';
+import {DateTimeMaskDocExample4} from './examples/4-time-step/component';
type GeneratorOptions = Required<
NonNullable
+ Property
+
+ Use
+ timeStep
+ allows you to increment / decrement time segments by pressing
+ ArrowUp
+ /
+ ArrowDown
+ .
+ step === 0
+ (default value) to disable this feature.
+
+ Default:
+ 0
+ (disable stepping)
+
+ Property
+ step
+ allows you to increment/decrement time segments by pressing
+ ArrowUp
+ /
+ ArrowDown
+ .
+
+ Use
+ step === 0
+ (default value) to disable this feature.
+
+ Default:
+ 0
+ (disable stepping)
+