From ba65070e8bb5f2d652d097601b1a20126331317b Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Fri, 9 Oct 2026 17:31:38 +0530 Subject: [PATCH 1/9] fix(jsonforms-renderers): store time picker values as HH:MM:SS Co-authored-by: Cursor --- packages/jsonforms-renderers/README.md | 2 +- packages/jsonforms-renderers/dev/fixtures.ts | 8 +- .../src/renderers/DateControl.test.tsx | 128 ++++++++++++++++++ .../src/renderers/DateControl.tsx | 32 ++++- 4 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 packages/jsonforms-renderers/src/renderers/DateControl.test.tsx diff --git a/packages/jsonforms-renderers/README.md b/packages/jsonforms-renderers/README.md index 03df5a5..c358e0d 100644 --- a/packages/jsonforms-renderers/README.md +++ b/packages/jsonforms-renderers/README.md @@ -15,7 +15,7 @@ This package provides a collection of JSON Forms controls and layout renderers t ### Controls -- **[DateControl](src/renderers/DateControl.tsx)**: Formats `date`, `time`, and `date-time` inputs. Combines native inputs that split space equally in `date-time` combination mode and expand to `100%` container width otherwise. +- **[DateControl](src/renderers/DateControl.tsx)**: Formats `date`, `time`, and `date-time` inputs. Time is stored as `HH:MM:SS`; set `options.showSeconds` to show a seconds spinner. - **[TextControl](src/renderers/TextControl.tsx)**: Handles standard string schemas with optional placeholders. - **[NumberControl](src/renderers/NumberControl.tsx)**: Handles numeric and integer inputs. - **[BooleanControl](src/renderers/BooleanControl.tsx)**: Toggle switch control for boolean values. diff --git a/packages/jsonforms-renderers/dev/fixtures.ts b/packages/jsonforms-renderers/dev/fixtures.ts index 5ab9640..777103c 100644 --- a/packages/jsonforms-renderers/dev/fixtures.ts +++ b/packages/jsonforms-renderers/dev/fixtures.ts @@ -421,7 +421,12 @@ export const fixtures: Fixture[] = [ properties: { eventDate: { type: 'string', format: 'date', description: 'Date only (yyyy-MM-dd)' }, appointment: { type: 'string', format: 'date-time', description: 'Date + time (RFC 3339)' }, - openingTime: { type: 'string', format: 'time', description: 'Time only (native picker)' }, + openingTime: { type: 'string', format: 'time', description: 'Time only (native picker, minute precision)' }, + closingTime: { + type: 'string', + format: 'time', + description: 'Time only with seconds spinner (options.showSeconds)', + }, }, required: ['eventDate'], }, @@ -431,6 +436,7 @@ export const fixtures: Fixture[] = [ { type: 'Control', scope: '#/properties/eventDate' }, { type: 'Control', scope: '#/properties/appointment' }, { type: 'Control', scope: '#/properties/openingTime' }, + { type: 'Control', scope: '#/properties/closingTime', options: { showSeconds: true } }, ], } as UISchemaElement, }, diff --git a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx new file mode 100644 index 0000000..ef15099 --- /dev/null +++ b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx @@ -0,0 +1,128 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it } from 'vitest' +import { useState } from 'react' +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { JsonForms } from '@jsonforms/react' +import { Theme } from '@radix-ui/themes' +import type { JsonSchema, UISchemaElement } from '@jsonforms/core' +import { radixRenderers } from './index' +import { timeInputValue, toRfc3339Time } from './DateControl' + +type Data = Record + +const finishers: (() => void)[] = [] + +afterEach(() => { + for (const stop of finishers) stop() + finishers.length = 0 + cleanup() +}) + +const schema: JsonSchema = { + type: 'object', + properties: { + openingTime: { type: 'string', format: 'time', title: 'Opening Time' }, + }, +} + +function renderForm(uischema: UISchemaElement, seed: Data) { + const writes: Data[] = [] + let live = true + finishers.push(() => { + live = false + }) + + function Harness() { + const [initial] = useState(seed) + return ( + + { + if (live) writes.push(data as Data) + }} + /> + + ) + } + + render() + return { writes } +} + +const timeControl = { + type: 'VerticalLayout', + elements: [{ type: 'Control', scope: '#/properties/openingTime' }], +} as UISchemaElement + +const secondsControl = { + type: 'VerticalLayout', + elements: [{ type: 'Control', scope: '#/properties/openingTime', options: { showSeconds: true } }], +} as UISchemaElement + +describe('toRfc3339Time', () => { + it('pads HH:MM with zero seconds', () => { + expect(toRfc3339Time('14:30')).toBe('14:30:00') + }) + + it('leaves HH:MM:SS unchanged', () => { + expect(toRfc3339Time('14:30:45')).toBe('14:30:45') + }) + + it('clears an empty picker value', () => { + expect(toRfc3339Time('')).toBeUndefined() + }) +}) + +describe('timeInputValue', () => { + it('drops seconds when the seconds spinner is off', () => { + expect(timeInputValue('14:30:00', false)).toBe('14:30') + }) + + it('keeps seconds when the seconds spinner is on', () => { + expect(timeInputValue('14:30:45', true)).toBe('14:30:45') + }) + + it('fills missing seconds for the seconds spinner', () => { + expect(timeInputValue('14:30', true)).toBe('14:30:00') + }) +}) + +describe('DateControl time', () => { + it('stores HH:MM:SS when the picker emits HH:MM', async () => { + const { writes } = renderForm(timeControl, {}) + const input = screen.getByLabelText(/Opening Time/) + + fireEvent.change(input, { target: { value: '14:30' } }) + + await waitFor(() => { + expect(writes.at(-1)).toEqual({ openingTime: '14:30:00' }) + }) + }) + + it('uses minute step unless showSeconds is set', () => { + renderForm(timeControl, { openingTime: '14:30:00' }) + expect(screen.getByLabelText(/Opening Time/).getAttribute('step')).toBe('60') + expect((screen.getByLabelText(/Opening Time/) as HTMLInputElement).value).toBe('14:30') + }) + + it('shows a seconds spinner when showSeconds is true', async () => { + const { writes } = renderForm(secondsControl, {}) + const input = screen.getByLabelText(/Opening Time/) + + expect(input.getAttribute('step')).toBe('1') + + fireEvent.change(input, { target: { value: '14:30' } }) + await waitFor(() => { + expect(writes.at(-1)).toEqual({ openingTime: '14:30:00' }) + }) + + fireEvent.change(input, { target: { value: '14:30:45' } }) + await waitFor(() => { + expect(writes.at(-1)).toEqual({ openingTime: '14:30:45' }) + }) + }) +}) diff --git a/packages/jsonforms-renderers/src/renderers/DateControl.tsx b/packages/jsonforms-renderers/src/renderers/DateControl.tsx index 4210cae..32caf5e 100644 --- a/packages/jsonforms-renderers/src/renderers/DateControl.tsx +++ b/packages/jsonforms-renderers/src/renderers/DateControl.tsx @@ -18,6 +18,23 @@ import { getErrorMessage } from '../utils/error' // seconds + local offset, which ajv's strict "date-time" format check requires. const toISODateTime = (dateStr: string, timeStr: string) => dayjs(`${dateStr}T${timeStr}`).format() +// AJV format "time" is RFC 3339, so seconds are required. Native +// with minute precision (the default) emits HH:MM; pad those to HH:MM:SS. +export const toRfc3339Time = (raw: string): string | undefined => { + if (!raw) return undefined + if (/^\d{2}:\d{2}$/.test(raw)) return `${raw}:00` + return raw +} + +// Native time inputs only understand HH:MM or HH:MM:SS. Strip timezone suffixes +// from stored RFC 3339 values, and drop seconds when the seconds spinner is off. +export const timeInputValue = (stored: string, showSeconds: boolean): string => { + if (!stored) return '' + const match = stored.match(/^(\d{2}:\d{2})(:\d{2})?/) + if (!match) return stored + return showSeconds ? `${match[1]}${match[2] ?? ':00'}` : match[1] +} + type ShellProps = Pick & { description?: string children: ReactNode @@ -55,6 +72,7 @@ export const DateControl = ({ required, errors, schema, + uischema, enabled, visible = true, }: ControlProps) => { @@ -69,6 +87,10 @@ export const DateControl = ({ } const isValid = errors.length === 0 const value: string = typeof data === 'string' ? data : '' + // Seconds spinner is opt-in. Omitted / false keeps hour+minute only. + // Stored values always include seconds so AJV format "time" still passes. + const showSeconds = uischema?.options?.showSeconds === true + const timeStep = showSeconds ? 1 : 60 const shell = (children: ReactNode) => ( @@ -81,8 +103,9 @@ export const DateControl = ({ return shell( handleChange(path, e.target.value)} + step={timeStep} + value={timeInputValue(value, showSeconds)} + onChange={(e) => handleChange(path, toRfc3339Time(e.target.value))} disabled={!enabled} color={!isValid ? 'red' : undefined} id={path} @@ -93,9 +116,7 @@ export const DateControl = ({ // date and date-time share the same date input; date-time appends a native time input. const hasTime = schema.format === 'date-time' const [datePart = '', timeRaw = ''] = value.split('T') - // The native only understands HH:MM(:SS); strip any - // timezone suffix from the stored RFC 3339 value before feeding it back. - const timeForInput = timeRaw.slice(0, 5) + const timeForInput = timeInputValue(timeRaw, showSeconds) const commit = (nextDate: string, nextTime: string) => { if (!nextDate) { @@ -119,6 +140,7 @@ export const DateControl = ({ {hasTime && ( Date: Fri, 9 Oct 2026 17:48:01 +0530 Subject: [PATCH 2/9] fix(jsonforms-renderers): move time helpers out of DateControl Co-authored-by: Cursor --- .../src/renderers/DateControl.test.tsx | 2 +- .../src/renderers/DateControl.tsx | 18 +----------------- packages/jsonforms-renderers/src/utils/time.ts | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 packages/jsonforms-renderers/src/utils/time.ts diff --git a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx index ef15099..ad6c49e 100644 --- a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx +++ b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx @@ -6,7 +6,7 @@ import { JsonForms } from '@jsonforms/react' import { Theme } from '@radix-ui/themes' import type { JsonSchema, UISchemaElement } from '@jsonforms/core' import { radixRenderers } from './index' -import { timeInputValue, toRfc3339Time } from './DateControl' +import { timeInputValue, toRfc3339Time } from '../utils/time' type Data = Record diff --git a/packages/jsonforms-renderers/src/renderers/DateControl.tsx b/packages/jsonforms-renderers/src/renderers/DateControl.tsx index 32caf5e..c685571 100644 --- a/packages/jsonforms-renderers/src/renderers/DateControl.tsx +++ b/packages/jsonforms-renderers/src/renderers/DateControl.tsx @@ -13,28 +13,12 @@ import { useEffect, type ReactNode } from 'react' import dayjs from 'dayjs' import { getErrorMessage } from '../utils/error' +import { timeInputValue, toRfc3339Time } from '../utils/time' // dayjs().format() defaults to RFC 3339 (e.g. 2026-06-05T12:30:00+05:30) with // seconds + local offset, which ajv's strict "date-time" format check requires. const toISODateTime = (dateStr: string, timeStr: string) => dayjs(`${dateStr}T${timeStr}`).format() -// AJV format "time" is RFC 3339, so seconds are required. Native -// with minute precision (the default) emits HH:MM; pad those to HH:MM:SS. -export const toRfc3339Time = (raw: string): string | undefined => { - if (!raw) return undefined - if (/^\d{2}:\d{2}$/.test(raw)) return `${raw}:00` - return raw -} - -// Native time inputs only understand HH:MM or HH:MM:SS. Strip timezone suffixes -// from stored RFC 3339 values, and drop seconds when the seconds spinner is off. -export const timeInputValue = (stored: string, showSeconds: boolean): string => { - if (!stored) return '' - const match = stored.match(/^(\d{2}:\d{2})(:\d{2})?/) - if (!match) return stored - return showSeconds ? `${match[1]}${match[2] ?? ':00'}` : match[1] -} - type ShellProps = Pick & { description?: string children: ReactNode diff --git a/packages/jsonforms-renderers/src/utils/time.ts b/packages/jsonforms-renderers/src/utils/time.ts new file mode 100644 index 0000000..adbe993 --- /dev/null +++ b/packages/jsonforms-renderers/src/utils/time.ts @@ -0,0 +1,16 @@ +// AJV format "time" is RFC 3339, so seconds are required. Native +// with minute precision (the default) emits HH:MM; pad those to HH:MM:SS. +export const toRfc3339Time = (raw: string): string | undefined => { + if (!raw) return undefined + if (/^\d{2}:\d{2}$/.test(raw)) return `${raw}:00` + return raw +} + +// Native time inputs only understand HH:MM or HH:MM:SS. Strip timezone suffixes +// from stored RFC 3339 values, and drop seconds when the seconds spinner is off. +export const timeInputValue = (stored: string, showSeconds: boolean): string => { + if (!stored) return '' + const match = stored.match(/^(\d{2}:\d{2})(:\d{2})?/) + if (!match) return stored + return showSeconds ? `${match[1]}${match[2] ?? ':00'}` : match[1] +} From 8570125f5fff6ae77e84a9aac4f46ab2f251bc4b Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Fri, 9 Oct 2026 23:18:11 +0530 Subject: [PATCH 3/9] test(jsonforms-renderers): colocate time helper tests in utils Co-authored-by: Cursor --- .../src/renderers/DateControl.test.tsx | 29 ------------------ .../src/utils/time.test.ts | 30 +++++++++++++++++++ 2 files changed, 30 insertions(+), 29 deletions(-) create mode 100644 packages/jsonforms-renderers/src/utils/time.test.ts diff --git a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx index ad6c49e..bc0456a 100644 --- a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx +++ b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx @@ -6,7 +6,6 @@ import { JsonForms } from '@jsonforms/react' import { Theme } from '@radix-ui/themes' import type { JsonSchema, UISchemaElement } from '@jsonforms/core' import { radixRenderers } from './index' -import { timeInputValue, toRfc3339Time } from '../utils/time' type Data = Record @@ -63,34 +62,6 @@ const secondsControl = { elements: [{ type: 'Control', scope: '#/properties/openingTime', options: { showSeconds: true } }], } as UISchemaElement -describe('toRfc3339Time', () => { - it('pads HH:MM with zero seconds', () => { - expect(toRfc3339Time('14:30')).toBe('14:30:00') - }) - - it('leaves HH:MM:SS unchanged', () => { - expect(toRfc3339Time('14:30:45')).toBe('14:30:45') - }) - - it('clears an empty picker value', () => { - expect(toRfc3339Time('')).toBeUndefined() - }) -}) - -describe('timeInputValue', () => { - it('drops seconds when the seconds spinner is off', () => { - expect(timeInputValue('14:30:00', false)).toBe('14:30') - }) - - it('keeps seconds when the seconds spinner is on', () => { - expect(timeInputValue('14:30:45', true)).toBe('14:30:45') - }) - - it('fills missing seconds for the seconds spinner', () => { - expect(timeInputValue('14:30', true)).toBe('14:30:00') - }) -}) - describe('DateControl time', () => { it('stores HH:MM:SS when the picker emits HH:MM', async () => { const { writes } = renderForm(timeControl, {}) diff --git a/packages/jsonforms-renderers/src/utils/time.test.ts b/packages/jsonforms-renderers/src/utils/time.test.ts new file mode 100644 index 0000000..28854bc --- /dev/null +++ b/packages/jsonforms-renderers/src/utils/time.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { timeInputValue, toRfc3339Time } from './time' + +describe('toRfc3339Time', () => { + it('pads HH:MM with zero seconds', () => { + expect(toRfc3339Time('14:30')).toBe('14:30:00') + }) + + it('leaves HH:MM:SS unchanged', () => { + expect(toRfc3339Time('14:30:45')).toBe('14:30:45') + }) + + it('clears an empty picker value', () => { + expect(toRfc3339Time('')).toBeUndefined() + }) +}) + +describe('timeInputValue', () => { + it('drops seconds when the seconds spinner is off', () => { + expect(timeInputValue('14:30:00', false)).toBe('14:30') + }) + + it('keeps seconds when the seconds spinner is on', () => { + expect(timeInputValue('14:30:45', true)).toBe('14:30:45') + }) + + it('fills missing seconds for the seconds spinner', () => { + expect(timeInputValue('14:30', true)).toBe('14:30:00') + }) +}) From 67bea1efe7b311041f7575f2a65ceb2ba5431f1d Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Fri, 9 Oct 2026 23:18:29 +0530 Subject: [PATCH 4/9] test(jsonforms-renderers): cover clearing time in DateControl Co-authored-by: Cursor --- .../src/renderers/DateControl.test.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx index bc0456a..6efdab6 100644 --- a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx +++ b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx @@ -74,6 +74,17 @@ describe('DateControl time', () => { }) }) + it('clears the time when the picker value is empty', async () => { + const { writes } = renderForm(timeControl, { openingTime: '14:30:00' }) + const input = screen.getByLabelText(/Opening Time/) + + fireEvent.change(input, { target: { value: '' } }) + + await waitFor(() => { + expect(writes.at(-1)?.openingTime).toBeUndefined() + }) + }) + it('uses minute step unless showSeconds is set', () => { renderForm(timeControl, { openingTime: '14:30:00' }) expect(screen.getByLabelText(/Opening Time/).getAttribute('step')).toBe('60') From 9930566a90e339fdcafff163af57a4fdd6cd66e6 Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Fri, 9 Oct 2026 23:18:47 +0530 Subject: [PATCH 5/9] docs(jsonforms-renderers): note date-time layout and showSeconds Co-authored-by: Cursor --- packages/jsonforms-renderers/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jsonforms-renderers/README.md b/packages/jsonforms-renderers/README.md index c358e0d..1c67739 100644 --- a/packages/jsonforms-renderers/README.md +++ b/packages/jsonforms-renderers/README.md @@ -15,7 +15,7 @@ This package provides a collection of JSON Forms controls and layout renderers t ### Controls -- **[DateControl](src/renderers/DateControl.tsx)**: Formats `date`, `time`, and `date-time` inputs. Time is stored as `HH:MM:SS`; set `options.showSeconds` to show a seconds spinner. +- **[DateControl](src/renderers/DateControl.tsx)**: Formats `date`, `time`, and `date-time` inputs. Combines native inputs that split space equally in `date-time` combination mode and expand to `100%` container width otherwise. Time values are stored as `HH:MM:SS`; set `options.showSeconds` on a `time` or `date-time` control to show a seconds spinner. - **[TextControl](src/renderers/TextControl.tsx)**: Handles standard string schemas with optional placeholders. - **[NumberControl](src/renderers/NumberControl.tsx)**: Handles numeric and integer inputs. - **[BooleanControl](src/renderers/BooleanControl.tsx)**: Toggle switch control for boolean values. From 1643e41a8c9cc831885951a74a07a468a2e73129 Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Fri, 9 Oct 2026 23:19:45 +0530 Subject: [PATCH 6/9] test(jsonforms-renderers): cover showSeconds on date-time controls Co-authored-by: Cursor --- .../src/renderers/DateControl.test.tsx | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx index 6efdab6..0e40d18 100644 --- a/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx +++ b/packages/jsonforms-renderers/src/renderers/DateControl.test.tsx @@ -17,14 +17,21 @@ afterEach(() => { cleanup() }) -const schema: JsonSchema = { +const openingTimeSchema: JsonSchema = { type: 'object', properties: { openingTime: { type: 'string', format: 'time', title: 'Opening Time' }, }, } -function renderForm(uischema: UISchemaElement, seed: Data) { +const appointmentSchema: JsonSchema = { + type: 'object', + properties: { + appointment: { type: 'string', format: 'date-time', title: 'Appointment' }, + }, +} + +function renderForm(schema: JsonSchema, uischema: UISchemaElement, seed: Data) { const writes: Data[] = [] let live = true finishers.push(() => { @@ -62,9 +69,19 @@ const secondsControl = { elements: [{ type: 'Control', scope: '#/properties/openingTime', options: { showSeconds: true } }], } as UISchemaElement +const appointmentControl = { + type: 'VerticalLayout', + elements: [{ type: 'Control', scope: '#/properties/appointment' }], +} as UISchemaElement + +const appointmentSecondsControl = { + type: 'VerticalLayout', + elements: [{ type: 'Control', scope: '#/properties/appointment', options: { showSeconds: true } }], +} as UISchemaElement + describe('DateControl time', () => { it('stores HH:MM:SS when the picker emits HH:MM', async () => { - const { writes } = renderForm(timeControl, {}) + const { writes } = renderForm(openingTimeSchema, timeControl, {}) const input = screen.getByLabelText(/Opening Time/) fireEvent.change(input, { target: { value: '14:30' } }) @@ -75,7 +92,7 @@ describe('DateControl time', () => { }) it('clears the time when the picker value is empty', async () => { - const { writes } = renderForm(timeControl, { openingTime: '14:30:00' }) + const { writes } = renderForm(openingTimeSchema, timeControl, { openingTime: '14:30:00' }) const input = screen.getByLabelText(/Opening Time/) fireEvent.change(input, { target: { value: '' } }) @@ -86,13 +103,13 @@ describe('DateControl time', () => { }) it('uses minute step unless showSeconds is set', () => { - renderForm(timeControl, { openingTime: '14:30:00' }) + renderForm(openingTimeSchema, timeControl, { openingTime: '14:30:00' }) expect(screen.getByLabelText(/Opening Time/).getAttribute('step')).toBe('60') expect((screen.getByLabelText(/Opening Time/) as HTMLInputElement).value).toBe('14:30') }) it('shows a seconds spinner when showSeconds is true', async () => { - const { writes } = renderForm(secondsControl, {}) + const { writes } = renderForm(openingTimeSchema, secondsControl, {}) const input = screen.getByLabelText(/Opening Time/) expect(input.getAttribute('step')).toBe('1') @@ -108,3 +125,19 @@ describe('DateControl time', () => { }) }) }) + +describe('DateControl date-time', () => { + it('uses minute step on the time input unless showSeconds is set', () => { + renderForm(appointmentSchema, appointmentControl, { appointment: '2026-10-09T14:30:00' }) + const timeInput = document.querySelector('input[type="time"]') + expect(timeInput?.getAttribute('step')).toBe('60') + expect((timeInput as HTMLInputElement).value).toBe('14:30') + }) + + it('shows a seconds spinner on date-time when showSeconds is true', () => { + renderForm(appointmentSchema, appointmentSecondsControl, { appointment: '2026-10-09T14:30:45' }) + const timeInput = document.querySelector('input[type="time"]') + expect(timeInput?.getAttribute('step')).toBe('1') + expect((timeInput as HTMLInputElement).value).toBe('14:30:45') + }) +}) From c7f00a4f07bcdb561d4b481aed3449023f45dcfb Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Fri, 9 Oct 2026 23:19:59 +0530 Subject: [PATCH 7/9] docs(jsonforms-renderers): document time formatting helpers Co-authored-by: Cursor --- packages/jsonforms-renderers/src/utils/time.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/jsonforms-renderers/src/utils/time.ts b/packages/jsonforms-renderers/src/utils/time.ts index adbe993..cf1438b 100644 --- a/packages/jsonforms-renderers/src/utils/time.ts +++ b/packages/jsonforms-renderers/src/utils/time.ts @@ -1,13 +1,11 @@ -// AJV format "time" is RFC 3339, so seconds are required. Native -// with minute precision (the default) emits HH:MM; pad those to HH:MM:SS. +/** Normalize a native time input value for AJV `format: "time"` (RFC 3339 `HH:MM:SS`). */ export const toRfc3339Time = (raw: string): string | undefined => { if (!raw) return undefined if (/^\d{2}:\d{2}$/.test(raw)) return `${raw}:00` return raw } -// Native time inputs only understand HH:MM or HH:MM:SS. Strip timezone suffixes -// from stored RFC 3339 values, and drop seconds when the seconds spinner is off. +/** Format a stored time for a native ``, optionally hiding seconds. */ export const timeInputValue = (stored: string, showSeconds: boolean): string => { if (!stored) return '' const match = stored.match(/^(\d{2}:\d{2})(:\d{2})?/) From c2cec5ea601b5ce9f6356a28cd93125d5cc06c1b Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Sat, 10 Oct 2026 12:03:25 +0530 Subject: [PATCH 8/9] docs(jsonforms-renderers): add DateControl guide under docs/ Co-authored-by: Cursor --- .../jsonforms-renderers/docs/date-control.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/jsonforms-renderers/docs/date-control.md diff --git a/packages/jsonforms-renderers/docs/date-control.md b/packages/jsonforms-renderers/docs/date-control.md new file mode 100644 index 0000000..8a0fdab --- /dev/null +++ b/packages/jsonforms-renderers/docs/date-control.md @@ -0,0 +1,40 @@ +# Date and time (`DateControl`) + +`DateControl` renders JSON Schema `string` fields with `format` of `date`, `time`, or `date-time`, using native browser pickers. + +See the `date` fixture in `dev/fixtures.ts` for a runnable example (`eventDate`, `appointment`, `openingTime`, `closingTime`). + +## Formats + +| Schema `format` | Stored value | UI | +| --------------- | ------------ | -- | +| `date` | `YYYY-MM-DD` | Native date input | +| `time` | RFC 3339 `HH:MM:SS` | Native time input | +| `date-time` | RFC 3339 date-time (seconds + local offset) | Date and time inputs side by side, sharing width equally | + +`time` and `date-time` share the same native time input. With `date` alone the date input expands to the full control width. + +## Stored time is always `HH:MM:SS` + +AJV's `format: "time"` is RFC 3339, which requires seconds. Native `` with minute precision (the default) emits `HH:MM`. `DateControl` pads that to `HH:MM:SS` before writing form data (`14:30` → `14:30:00`) via [`utils/time.ts`](../src/utils/time.ts). Clearing the picker still clears the field. + +Artifact schemas that declare `format: "time"` do not need to change. + +## Seconds spinner (`options.showSeconds`) + +The seconds spinner is **opt-in**. Set `options.showSeconds: true` on the **Control** in the uischema — it applies to both `time` and `date-time`: + +```json +{ + "type": "Control", + "scope": "#/properties/proposed_inspection_time", + "options": { "showSeconds": true } +} +``` + +| `showSeconds` | Picker | Stored value | +| ------------- | ------ | ------------ | +| omitted / `false` | Hour and minute only (`step=60`) | Seconds default to `00` | +| `true` | Hour, minute, and seconds (`step=1`) | Seconds as picked; still `00` if the user only sets hour and minute | + +A form that omits `showSeconds` keeps the hour-and-minute picker and still submits a valid `HH:MM:SS` string. From 200473e50ede79121e35eb324ac73c509a1545e1 Mon Sep 17 00:00:00 2001 From: Janitha Karunarathna Date: Sat, 10 Oct 2026 12:09:17 +0530 Subject: [PATCH 9/9] style(jsonforms-renderers): prettier-format date-control.md Co-authored-by: Cursor --- .../jsonforms-renderers/docs/date-control.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/jsonforms-renderers/docs/date-control.md b/packages/jsonforms-renderers/docs/date-control.md index 8a0fdab..8aa5e46 100644 --- a/packages/jsonforms-renderers/docs/date-control.md +++ b/packages/jsonforms-renderers/docs/date-control.md @@ -6,11 +6,11 @@ See the `date` fixture in `dev/fixtures.ts` for a runnable example (`eventDate`, ## Formats -| Schema `format` | Stored value | UI | -| --------------- | ------------ | -- | -| `date` | `YYYY-MM-DD` | Native date input | -| `time` | RFC 3339 `HH:MM:SS` | Native time input | -| `date-time` | RFC 3339 date-time (seconds + local offset) | Date and time inputs side by side, sharing width equally | +| Schema `format` | Stored value | UI | +| --------------- | ------------------------------------------- | -------------------------------------------------------- | +| `date` | `YYYY-MM-DD` | Native date input | +| `time` | RFC 3339 `HH:MM:SS` | Native time input | +| `date-time` | RFC 3339 date-time (seconds + local offset) | Date and time inputs side by side, sharing width equally | `time` and `date-time` share the same native time input. With `date` alone the date input expands to the full control width. @@ -32,9 +32,9 @@ The seconds spinner is **opt-in**. Set `options.showSeconds: true` on the **Cont } ``` -| `showSeconds` | Picker | Stored value | -| ------------- | ------ | ------------ | -| omitted / `false` | Hour and minute only (`step=60`) | Seconds default to `00` | -| `true` | Hour, minute, and seconds (`step=1`) | Seconds as picked; still `00` if the user only sets hour and minute | +| `showSeconds` | Picker | Stored value | +| ----------------- | ------------------------------------ | ------------------------------------------------------------------- | +| omitted / `false` | Hour and minute only (`step=60`) | Seconds default to `00` | +| `true` | Hour, minute, and seconds (`step=1`) | Seconds as picked; still `00` if the user only sets hour and minute | A form that omits `showSeconds` keeps the hour-and-minute picker and still submits a valid `HH:MM:SS` string.