From 8cddaba83793333d75290fcb08c0a0d4a902c936 Mon Sep 17 00:00:00 2001 From: Armaan Gupta Date: Wed, 17 Jun 2026 16:10:23 +0530 Subject: [PATCH 1/4] feat(dateTime): added datetimeinput component in react vanilla components --- .../components/DateTimeInput.test.tsx | 198 ++++++++++++++++++ .../src/components/DateTimeInput.tsx | 98 +++++++++ .../react-vanilla-components/src/index.ts | 2 + .../src/utils/mappings.ts | 3 + 4 files changed, 301 insertions(+) create mode 100644 packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx create mode 100644 packages/react-vanilla-components/src/components/DateTimeInput.tsx diff --git a/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx b/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx new file mode 100644 index 00000000..8f24ca9c --- /dev/null +++ b/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright 2023 Adobe, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import DateTimeInput from '../../src/components/DateTimeInput'; +import userEvent from '@testing-library/user-event'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { renderComponent, DEFAULT_ERROR_MESSAGE } from '../utils'; +import '@testing-library/jest-dom/extend-expect'; + +const field = { + name: 'datetime1', + label: { value: 'Date and Time', visible: true }, + fieldType: 'datetime-input', + type: 'string', + format: 'date-time', + visible: true, + required: true, + enabled: true, + readOnly: false, +}; + +// Full CRISP JSON with top-level minimum/maximum (af-core 0.22.94+ preserves and validates these). +const fullField = { + id: 'datetime-6a4986a577', + fieldType: 'datetime-input', + name: 'datetime1780381446201', + visible: true, + description: '

long

', + tooltip: '

short

', + type: 'string', + required: true, + enabled: true, + constraintMessages: { required: 'wrong date', minimum: 'minimum date', maximum: 'max date' }, + readOnly: false, + default: '2026-05-06 12:30:25', + format: 'date-time', + label: { visible: true, value: 'Date and Time' }, + minimum: '2025-11-04T12:55', + maximum: '2026-06-27T12:55', + properties: { + 'afs:layout': { tooltipVisible: false }, + 'fd:path': '/content/forms/af/table-component/jcr:content/guideContainer/datetime', + }, +}; + +const helper = renderComponent(DateTimeInput); + +describe('DateTimeInput', () => { + + test('value changed by user is set in model', async () => { + const { renderResponse, element } = await helper(field); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + fireEvent.change(input, { target: { value: '2024-06-15T10:30' } }); + expect(element.getState().value).toEqual('2024-06-15T10:30'); + }); + + test('it should handle visible property', async () => { + const f = { ...field, visible: false }; + const { renderResponse } = await helper(f); + expect(renderResponse.queryByText(field.label.value)).toBeNull(); + }); + + test('error message element exists when the field is invalid', async () => { + const f = { ...field, valid: false, errorMessage: DEFAULT_ERROR_MESSAGE }; + const { renderResponse } = await helper(f); + expect(renderResponse.queryByText(DEFAULT_ERROR_MESSAGE)).not.toBeNull(); + }); + + test('labels and inputs are linked with for and id attribute', async () => { + const { renderResponse } = await helper(field); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + const label = renderResponse.queryByText(field.label.value); + expect(input?.getAttribute('id')).toEqual(label?.getAttribute('for')); + }); + + test('input renders with datetime-local type', async () => { + const { renderResponse } = await helper(field); + const input = renderResponse.container.querySelector('input[type="datetime-local"]'); + expect(input).not.toBeNull(); + }); + + test('disabled attribute is set when enabled is false', async () => { + const f = { ...field, enabled: false }; + const { renderResponse } = await helper(f); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + expect(input).toBeDisabled(); + }); + + test('default value with space separator is normalised to datetime-local format', async () => { + // af-core surfaces default as "YYYY-MM-DD HH:MM:SS"; datetime-local requires "YYYY-MM-DDTHH:MM" + const { renderResponse } = await helper(fullField); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + expect(input.value).toEqual('2026-05-06T12:30'); + }); + + test('min and max HTML attributes are set from JSON minimum/maximum', async () => { + const { renderResponse } = await helper(fullField); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + expect(input).toHaveAttribute('min', '2025-11-04T12:55'); + expect(input).toHaveAttribute('max', '2026-06-27T12:55'); + }); + + test('minimum constraint message is shown when value is below minimum', async () => { + const f = { + ...field, + minimum: '2025-11-04T12:55', + maximum: '2026-06-27T12:55', + constraintMessages: { minimum: 'minimum date', maximum: 'max date' }, + }; + const { renderResponse } = await helper(f); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + fireEvent.change(input, { target: { value: '2020-01-01T10:00' } }); + fireEvent.blur(input); + await waitFor(() => { + expect(renderResponse.queryByText('minimum date')).not.toBeNull(); + }); + }); + + test('maximum constraint message is shown when value is above maximum', async () => { + const f = { + ...field, + minimum: '2025-11-04T12:55', + maximum: '2026-06-27T12:55', + constraintMessages: { minimum: 'minimum date', maximum: 'max date' }, + }; + const { renderResponse } = await helper(f); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + fireEvent.change(input, { target: { value: '2027-01-01T10:00' } }); + fireEvent.blur(input); + await waitFor(() => { + expect(renderResponse.queryByText('max date')).not.toBeNull(); + }); + }); + + test('tooltip and description toggle correctly', async () => { + const f = { + ...field, + tooltip: 'Short Description', + description: 'Long Description', + properties: { 'afs:layout': { tooltipVisible: true } }, + }; + const { renderResponse } = await helper(f); + expect(renderResponse.getByText('Short Description')).not.toBeNull(); + const button = renderResponse.container.getElementsByClassName('cmp-adaptiveform-datetimeinput__questionmark'); + userEvent.click(button[0]); + expect(renderResponse.getByText('Long Description')).not.toBeNull(); + }); + + test('aria-describedby contains long and short description ids when both are present', async () => { + const f = { + ...field, + id: 'datetime-123', + tooltip: 'short desc', + description: 'long desc', + properties: { 'afs:layout': { tooltipVisible: true } }, + }; + const { renderResponse } = await helper(f); + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; + expect(input).toHaveAttribute( + 'aria-describedby', + `${f.id}__longdescription ${f.id}__shortdescription` + ); + }); + + test('html in the label should be rendered for rich text', async () => { + const f = { + ...field, + label: { value: 'Date and Time', richText: true, visible: true }, + }; + const { renderResponse } = await helper(f); + expect(renderResponse.container.innerHTML).toContain('Date and Time'); + }); + + test('required constraint message is shown on validation failure', async () => { + const f = { ...fullField, valid: false, errorMessage: 'wrong date' }; + const { renderResponse } = await helper(f); + expect(renderResponse.queryByText('wrong date')).not.toBeNull(); + }); + + test('full CRISP JSON field renders without errors', async () => { + const { renderResponse } = await helper(fullField); + expect(renderResponse.container.querySelector('input[type="datetime-local"]')).not.toBeNull(); + expect(renderResponse.queryByText('Date and Time')).not.toBeNull(); + }); +}); diff --git a/packages/react-vanilla-components/src/components/DateTimeInput.tsx b/packages/react-vanilla-components/src/components/DateTimeInput.tsx new file mode 100644 index 00000000..300b82cc --- /dev/null +++ b/packages/react-vanilla-components/src/components/DateTimeInput.tsx @@ -0,0 +1,98 @@ +// ******************************************************************************* +// * Copyright 2023 Adobe +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// * +// * BEM markup follows AEM core form components guidelines. +// * LINK- https://github.com/adobe/aem-core-forms-components +// ****************************************************************************** + +import React, { useCallback } from 'react'; +import { withRuleEngine } from '../utils/withRuleEngine'; +import { PROPS } from '../utils/type'; +import FieldWrapper from './common/FieldWrapper'; +import { syncAriaDescribedBy } from '../utils/utils'; + +// datetime-local requires "YYYY-MM-DDTHH:MM". af-core may surface the default +// or user-entered value with a space separator ("YYYY-MM-DD HH:MM:SS"), so we +// normalise to the T-separated format and strip seconds. +const toDateTimeLocalValue = (val: string | undefined): string => { + if (!val) { return ''; } + const normalised = val.replace(' ', 'T'); + return normalised.length > 16 ? normalised.slice(0, 16) : normalised; +}; + +const datetime = (props: PROPS) => { + const { + id, label, value, required, name, readOnly, + placeholder, visible, enabled, appliedCssClassNames, valid, minimum, maximum + } = props; + + const finalValue = toDateTimeLocalValue(value as string | undefined); + + const handleChange = useCallback((e: React.ChangeEvent) => { + props.dispatchChange(e.target.value); + }, [props.dispatchChange]); + + const handleFocus = useCallback(() => { + props.dispatchFocus(); + }, [props.dispatchFocus]); + + const handleBlur = useCallback(() => { + props.dispatchBlur(); + }, [props.dispatchBlur]); + + return ( +
+ + + +
+ ); +}; + +export default withRuleEngine(datetime); diff --git a/packages/react-vanilla-components/src/index.ts b/packages/react-vanilla-components/src/index.ts index 8e414725..dbbaa2a4 100644 --- a/packages/react-vanilla-components/src/index.ts +++ b/packages/react-vanilla-components/src/index.ts @@ -12,6 +12,7 @@ import RadioGroup from './components/RadioButtonGroup'; import DropDown from './components/DropDown'; import NumberField from './components/NumberField'; import DateInput from './components/DateInput'; +import DateTimeInput from './components/DateTimeInput'; import CheckBox from './components/CheckBox'; import Button from './components/Button'; import TextFieldArea from './components/TextFieldArea'; @@ -42,6 +43,7 @@ export { DropDown, NumberField, DateInput, + DateTimeInput, Button, CheckBox, FileUpload, diff --git a/packages/react-vanilla-components/src/utils/mappings.ts b/packages/react-vanilla-components/src/utils/mappings.ts index 614df5aa..478a9e1b 100644 --- a/packages/react-vanilla-components/src/utils/mappings.ts +++ b/packages/react-vanilla-components/src/utils/mappings.ts @@ -18,6 +18,7 @@ import Button from '../components/Button'; import CheckBox from '../components/CheckBox'; import CheckBoxGroup from '../components/CheckBoxGroup'; import DateInput from '../components/DateInput'; +import DateTimeInput from '../components/DateTimeInput'; import DropDown from '../components/DropDown'; import NumberField from '../components/NumberField'; import RadioGroup from '../components/RadioButtonGroup'; @@ -46,6 +47,8 @@ const mappings = { 'drop-down': DropDown, 'number-input': NumberField, 'date-input': DateInput, + 'datetime-input': DateTimeInput, + 'forms-components-examples/components/form/datetime': DateTimeInput, button: Button, checkbox: CheckBox, 'file-input': FileUpload, From c7b47ec1c3fd3d54b12473c6b12f0266149e05c2 Mon Sep 17 00:00:00 2001 From: Armaan Gupta Date: Fri, 3 Jul 2026 16:27:45 +0530 Subject: [PATCH 2/4] address DateTimeInput review comments: BEM/DOM parity, casing, mapping fix - Fix test to look up cmp-adaptiveform-datetime__questionmark, matching the component's actual bemBlock (was cmp-adaptiveform-datetimeinput, so the lookup returned nothing and userEvent.click(undefined) threw). - Wrap the in a cmp-adaptiveform-datetime__input-wrapper div to match the Core datetime.html markup/CSS contract. - Fix data-cmp-is casing to adaptiveFormDatetime (capital D), matching Core. - Replace the examples/demo resourceType left over from local testing with the actual Core resourceType, core/fd/components/form/datetime/v1/datetime. Co-Authored-By: Claude Sonnet 5 --- .../components/DateTimeInput.test.tsx | 2 +- .../src/components/DateTimeInput.tsx | 44 ++++++++++--------- .../src/utils/mappings.ts | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx b/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx index 8f24ca9c..4765f4d4 100644 --- a/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx +++ b/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx @@ -154,7 +154,7 @@ describe('DateTimeInput', () => { }; const { renderResponse } = await helper(f); expect(renderResponse.getByText('Short Description')).not.toBeNull(); - const button = renderResponse.container.getElementsByClassName('cmp-adaptiveform-datetimeinput__questionmark'); + const button = renderResponse.container.getElementsByClassName('cmp-adaptiveform-datetime__questionmark'); userEvent.click(button[0]); expect(renderResponse.getByText('Long Description')).not.toBeNull(); }); diff --git a/packages/react-vanilla-components/src/components/DateTimeInput.tsx b/packages/react-vanilla-components/src/components/DateTimeInput.tsx index 300b82cc..590b90e3 100644 --- a/packages/react-vanilla-components/src/components/DateTimeInput.tsx +++ b/packages/react-vanilla-components/src/components/DateTimeInput.tsx @@ -55,7 +55,7 @@ const datetime = (props: PROPS) => { return (
{ isError={props.isError} errorMessage={props.errorMessage} > - +
+ +
); diff --git a/packages/react-vanilla-components/src/utils/mappings.ts b/packages/react-vanilla-components/src/utils/mappings.ts index 478a9e1b..90b887a7 100644 --- a/packages/react-vanilla-components/src/utils/mappings.ts +++ b/packages/react-vanilla-components/src/utils/mappings.ts @@ -48,7 +48,7 @@ const mappings = { 'number-input': NumberField, 'date-input': DateInput, 'datetime-input': DateTimeInput, - 'forms-components-examples/components/form/datetime': DateTimeInput, + 'core/fd/components/form/datetime/v1/datetime': DateTimeInput, button: Button, checkbox: CheckBox, 'file-input': FileUpload, From cd613df1522cbe68bee8f2fbf7eb125863d1d9bb Mon Sep 17 00:00:00 2001 From: Armaan Gupta Date: Fri, 3 Jul 2026 16:29:33 +0530 Subject: [PATCH 3/4] updated the year to 2026 --- .../react-vanilla-components/src/components/DateTimeInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-vanilla-components/src/components/DateTimeInput.tsx b/packages/react-vanilla-components/src/components/DateTimeInput.tsx index 590b90e3..0cb79fd9 100644 --- a/packages/react-vanilla-components/src/components/DateTimeInput.tsx +++ b/packages/react-vanilla-components/src/components/DateTimeInput.tsx @@ -1,5 +1,5 @@ // ******************************************************************************* -// * Copyright 2023 Adobe +// * Copyright 2026 Adobe // * // * Licensed under the Apache License, Version 2.0 (the "License"); // * you may not use this file except in compliance with the License. From 939896c559e52838233df0c7c61f71c54ec88894 Mon Sep 17 00:00:00 2001 From: Armaan Gupta Date: Fri, 3 Jul 2026 16:31:42 +0530 Subject: [PATCH 4/4] updated the year to 2026 --- .../__tests__/components/DateTimeInput.test.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx b/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx index 4765f4d4..dc033dc1 100644 --- a/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx +++ b/packages/react-vanilla-components/__tests__/components/DateTimeInput.test.tsx @@ -1,5 +1,5 @@ -/* - * Copyright 2023 Adobe, Inc. +/* ************************************************************************ + * Copyright 2026 Adobe, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ + *************************************************************************/ import DateTimeInput from '../../src/components/DateTimeInput'; import userEvent from '@testing-library/user-event'; @@ -32,7 +32,6 @@ const field = { readOnly: false, }; -// Full CRISP JSON with top-level minimum/maximum (af-core 0.22.94+ preserves and validates these). const fullField = { id: 'datetime-6a4986a577', fieldType: 'datetime-input',