Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(legacy): InputTime & InputDateTime support AM / PM formats #9595

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 63 additions & 11 deletions projects/cdk/date-time/test/time.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,35 @@ describe('TuiTime', () => {
expect(time.seconds).toBe(0);
expect(time.ms).toBe(888);
});

describe('mode with AM / PM', () => {
(
[
['12:00 AM', {hours: 0, minutes: 0}],
['12:34 AM', {hours: 0, minutes: 34}],
['12:59 AM', {hours: 0, minutes: 59}],
['01:00 AM', {hours: 1, minutes: 0}],
['11:00 AM', {hours: 11, minutes: 0}],
['11:59 AM', {hours: 11, minutes: 59}],
['12:00 PM', {hours: 12, minutes: 0}],
['12:01 PM', {hours: 12, minutes: 1}],
['12:59 PM', {hours: 12, minutes: 59}],
['01:00 PM', {hours: 13, minutes: 0}],
['11:00 PM', {hours: 23, minutes: 0}],
['11:59 PM', {hours: 23, minutes: 59}],
['04:59', {hours: 4, minutes: 59}],
] as const
).forEach(([timeString, {hours, minutes}]) => {
it(`from ${timeString}`, () => {
const time = TuiTime.fromString(timeString);

expect(time.hours).toBe(hours);
expect(time.minutes).toBe(minutes);
expect(time.seconds).toBe(0);
expect(time.ms).toBe(0);
});
});
});
});

describe('current', () => {
Expand Down Expand Up @@ -334,22 +363,45 @@ describe('TuiTime', () => {
});
});

it('stringify', () => {
const time = new TuiTime(6, 36, 1, 1);
describe('toString(mode) method', () => {
it('without mode parameter', () => {
const time = new TuiTime(6, 36, 1, 1);

expect(time.toString()).toBe('06:36:01.001');
});
expect(time.toString()).toBe('06:36:01.001');
});

it('stringify and fill zeros for seconds', () => {
const time = new TuiTime(6, 36, 0, 0);
it('stringify and fill zeros for seconds', () => {
const time = new TuiTime(6, 36, 0, 0);

expect(time.toString('HH:MM:SS')).toBe('06:36:00');
});
expect(time.toString('HH:MM:SS')).toBe('06:36:00');
});

it('stringify and fill zeros for seconds and ms', () => {
const time = new TuiTime(6, 36, 0, 0);
it('stringify and fill zeros for seconds and ms', () => {
const time = new TuiTime(6, 36, 0, 0);

expect(time.toString('HH:MM:SS.MSS')).toBe('06:36:00.000');
expect(time.toString('HH:MM:SS.MSS')).toBe('06:36:00.000');
});

describe('HH:MM AA', () => {
(
[
[new TuiTime(0, 0), '12:00 AM'],
[new TuiTime(0, 30), '12:30 AM'],
[new TuiTime(0, 59), '12:59 AM'],
[new TuiTime(1, 1), '01:01 AM'],
[new TuiTime(11, 11), '11:11 AM'],
[new TuiTime(11, 59), '11:59 AM'],
[new TuiTime(12, 0), '12:00 PM'],
[new TuiTime(13, 0), '01:00 PM'],
[new TuiTime(16, 0), '04:00 PM'],
[new TuiTime(23, 59), '11:59 PM'],
] as const
).forEach(([time, timeString]) => {
it(`{hours: ${time.hours}, minutes: ${time.minutes}} => ${timeString}`, () => {
expect(time.toString('HH:MM AA')).toBe(timeString);
});
});
});
});

describe('valueOf returns', () => {
Expand Down
48 changes: 43 additions & 5 deletions projects/cdk/date-time/time.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="@taiga-ui/tsconfig/ng-dev-mode" />

import {CHAR_NO_BREAK_SPACE} from '@taiga-ui/cdk/constants';
import {tuiInRange} from '@taiga-ui/cdk/utils/math';

import {
Expand Down Expand Up @@ -113,7 +114,7 @@ export class TuiTime implements TuiTimeLike {
* Parses string into TuiTime object
*/
public static fromString(time: string): TuiTime {
const hours = Number(time.slice(0, 2));
const hours = this.parseHours(time);
const minutes = Number(time.slice(3, 5)) || 0;
const seconds = Number(time.slice(6, 8)) || 0;
const ms = Number(time.slice(9, 12)) || 0;
Expand All @@ -134,6 +135,29 @@ export class TuiTime implements TuiTimeLike {
);
}

private static parseMeridiemPeriod(time: string): 'AM' | 'PM' | null {
return (
(/[AP]M/.exec(time.toUpperCase().replaceAll(/\W/g, ''))?.[0] as
| 'AM'
| 'PM') || null
);
}

private static parseHours(time: string): number {
const hours = Number(time.slice(0, 2));
const meridiem = this.parseMeridiemPeriod(time);

if (!meridiem) {
return hours;
}

if (hours === 12) {
return meridiem === 'AM' ? 0 : 12;
}

return meridiem === 'PM' ? hours + 12 : hours;
}

/**
* Shifts time by hours and minutes
*/
Expand Down Expand Up @@ -165,14 +189,18 @@ export class TuiTime implements TuiTimeLike {
* Converts TuiTime to string
*/
public toString(mode?: TuiTimeMode): string {
const needAddMs = mode === 'HH:MM:SS.MSS' || (!mode && this.ms > 0);
const needAddMs = mode?.startsWith('HH:MM:SS.MSS') || (!mode && this.ms > 0);
const needAddSeconds =
needAddMs || mode === 'HH:MM:SS' || (!mode && this.seconds > 0);
const hhMm = `${this.formatTime(this.hours)}:${this.formatTime(this.minutes)}`;
needAddMs || mode?.startsWith('HH:MM:SS') || (!mode && this.seconds > 0);
const {hours = this.hours, meridiem = ''} = mode?.includes('AA')
? this.toTwelveHour(this.hours)
: {};
const hhMm = `${this.formatTime(hours)}:${this.formatTime(this.minutes)}`;
const ss = needAddSeconds ? `:${this.formatTime(this.seconds)}` : '';
const mss = needAddMs ? `.${this.formatTime(this.ms, 3)}` : '';
const aa = meridiem && `${CHAR_NO_BREAK_SPACE}${meridiem}`;

return `${hhMm}${ss}${mss}`;
return `${hhMm}${ss}${mss}${aa}`;
}

public valueOf(): number {
Expand Down Expand Up @@ -203,4 +231,14 @@ export class TuiTime implements TuiTimeLike {
private formatTime(time: number, digits = 2): string {
return String(time).padStart(digits, '0');
}

private toTwelveHour(hours: number): {hours: number; meridiem: string} {
const meridiem = hours >= 12 ? 'PM' : 'AM';

if (hours === 0 || hours === 12) {
return {meridiem, hours: 12};
}

return {meridiem, hours: hours % 12};
}
}
8 changes: 7 additions & 1 deletion projects/cdk/date-time/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
* * YMD - yyyy.mm.dd
*/
export type TuiDateMode = 'DMY' | 'MDY' | 'YMD';
export type TuiTimeMode = 'HH:MM:SS.MSS' | 'HH:MM:SS' | 'HH:MM';
export type TuiTimeMode =
| 'HH:MM AA'
| 'HH:MM:SS AA'
| 'HH:MM:SS.MSS AA'
| 'HH:MM:SS.MSS'
| 'HH:MM:SS'
| 'HH:MM';

/**
* Optionally has year and/or month and/or day
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,19 @@
);

await timeModeSelect.textfield.click();
await timeModeSelect.selectOptions([1]);
await timeModeSelect.selectOptions([2]);

await expect(timeModeSelect.textfield).toHaveValue('HH:MM:SS');

Check failure on line 179 in projects/demo-playwright/tests/legacy/input-date-time/input-date-time.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / (7 of 9)

[chromium] › tests/legacy/input-date-time/input-date-time.spec.ts:165:13 › InputDateTime › API page › change filler on dynamic change of [timeMode] prop

1) [chromium] › tests/legacy/input-date-time/input-date-time.spec.ts:165:13 › InputDateTime › API page › change filler on dynamic change of [timeMode] prop Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('.t-table .t-row:has-text("[timeMode]")').locator('.t-cell_value tui-select').first().getByRole('textbox') Expected string: "HH:MM:SS" Received string: "HH:MM:SS.MSS" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('.t-table .t-row:has-text("[timeMode]")').locator('.t-cell_value tui-select').first().getByRole('textbox') - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" 177 | await timeModeSelect.selectOptions([2]); 178 | > 179 | await expect(timeModeSelect.textfield).toHaveValue('HH:MM:SS'); | ^ 180 | 181 | await inputDateTime.textfield.focus(); 182 | at /home/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-date-time/input-date-time.spec.ts:179:52

Check failure on line 179 in projects/demo-playwright/tests/legacy/input-date-time/input-date-time.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / (7 of 9)

[chromium] › tests/legacy/input-date-time/input-date-time.spec.ts:165:13 › InputDateTime › API page › change filler on dynamic change of [timeMode] prop

1) [chromium] › tests/legacy/input-date-time/input-date-time.spec.ts:165:13 › InputDateTime › API page › change filler on dynamic change of [timeMode] prop Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('.t-table .t-row:has-text("[timeMode]")').locator('.t-cell_value tui-select').first().getByRole('textbox') Expected string: "HH:MM:SS" Received string: "HH:MM:SS.MSS" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('.t-table .t-row:has-text("[timeMode]")').locator('.t-cell_value tui-select').first().getByRole('textbox') - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" 177 | await timeModeSelect.selectOptions([2]); 178 | > 179 | await expect(timeModeSelect.textfield).toHaveValue('HH:MM:SS'); | ^ 180 | 181 | await inputDateTime.textfield.focus(); 182 | at /home/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-date-time/input-date-time.spec.ts:179:52

Check failure on line 179 in projects/demo-playwright/tests/legacy/input-date-time/input-date-time.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / (7 of 9)

[chromium] › tests/legacy/input-date-time/input-date-time.spec.ts:165:13 › InputDateTime › API page › change filler on dynamic change of [timeMode] prop

1) [chromium] › tests/legacy/input-date-time/input-date-time.spec.ts:165:13 › InputDateTime › API page › change filler on dynamic change of [timeMode] prop Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('.t-table .t-row:has-text("[timeMode]")').locator('.t-cell_value tui-select').first().getByRole('textbox') Expected string: "HH:MM:SS" Received string: "HH:MM:SS.MSS" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('.t-table .t-row:has-text("[timeMode]")').locator('.t-cell_value tui-select').first().getByRole('textbox') - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" - locator resolved to <input readonly tabindex="0" id="[timeMode]" aria-invalid="false" _ngcontent-ng-c3831396199="" class="t-input ng-pristine ng-valid ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "HH:MM:SS.MSS" 177 | await timeModeSelect.selectOptions([2]); 178 | > 179 | await expect(timeModeSelect.textfield).toHaveValue('HH:MM:SS'); | ^ 180 | 181 | await inputDateTime.textfield.focus(); 182 | at /home/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-date-time/input-date-time.spec.ts:179:52

await inputDateTime.textfield.focus();

await expect(inputDateTime.host).toHaveScreenshot('03-timeMode=HH:MM.SS.png');

await timeModeSelect.textfield.click();
await timeModeSelect.selectOptions([2]);
await timeModeSelect.selectOptions([4]);

await expect(timeModeSelect.textfield).toHaveValue('HH:MM:SS.MSS');

await inputDateTime.textfield.focus();

await expect(inputDateTime.host).toHaveScreenshot(
Expand All @@ -196,6 +202,27 @@

await expect(inputDateTime.textfield).toHaveValue('07.06.2024, 23:59:00.000');
});

test.describe('AM / PM', () => {
test.beforeEach(async ({page}) => {
await tuiGoto(page, `${DemoRoute.InputDateTime}/API?timeMode=HH:MM%20AA`);
await inputDateTime.textfield.pressSequentially('2092020');

await expect(inputDateTime.textfield).toHaveValue('20.09.2020');
});

test('330a => 03:30 AM', async () => {
await inputDateTime.textfield.pressSequentially('330a');

await expect(inputDateTime.textfield).toHaveValue('20.09.2020, 03:30 AM');
});

test('330p => 03:30 PM', async () => {
await inputDateTime.textfield.pressSequentially('330p');

await expect(inputDateTime.textfield).toHaveValue('20.09.2020, 03:30 PM');
});
});
});

test.describe('invalid date', () => {
Expand Down
Loading
Loading