diff --git a/packages/components/news/8006.feature b/packages/components/news/8006.feature new file mode 100644 index 00000000000..459e0e672a5 --- /dev/null +++ b/packages/components/news/8006.feature @@ -0,0 +1 @@ +Use plone components for date/time widgets @tedw87 \ No newline at end of file diff --git a/packages/volto/news/8006.feature b/packages/volto/news/8006.feature new file mode 100644 index 00000000000..f905bd658b5 --- /dev/null +++ b/packages/volto/news/8006.feature @@ -0,0 +1 @@ +Replace DatetimeWidget and TimeWidget with react-aria-components from @plone/components, removing moment.js dependency from these widgets. @tedw87 diff --git a/packages/volto/package.json b/packages/volto/package.json index 73a1eee78f2..22e8429474c 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -117,6 +117,7 @@ "node": "^22 || ^24" }, "dependencies": { + "@internationalized/date": "^3.10.1", "@dnd-kit/core": "6.0.8", "@dnd-kit/sortable": "7.0.2", "@dnd-kit/utilities": "3.2.2", @@ -163,6 +164,7 @@ "query-string": "^9.0.0", "rc-time-picker": "3.7.3", "react": "18.2.0", + "react-aria-components": "^1.14.0", "react-anchor-link-smooth-scroll": "1.0.12", "react-animate-height": "2.0.17", "react-beautiful-dnd": "13.0.0", diff --git a/packages/volto/src/components/manage/Controlpanels/__snapshots__/Aliases.test.jsx.snap b/packages/volto/src/components/manage/Controlpanels/__snapshots__/Aliases.test.jsx.snap index 74fc1976763..5b9f8d65875 100644 --- a/packages/volto/src/components/manage/Controlpanels/__snapshots__/Aliases.test.jsx.snap +++ b/packages/volto/src/components/manage/Controlpanels/__snapshots__/Aliases.test.jsx.snap @@ -192,10 +192,364 @@ exports[`Aliases > renders an aliases control component 1`] = `
+ > +
+
+
+
+
+ +
+
+
+
+
+
+ + + +
+
+ +
+
+
+
+
+
+ > +
+
+
+
+
+ +
+
+
+
+
+
+ + + +
+
+ +
+
+
+
+
+
+ + + + + + {resettable && dateValue && ( )} @@ -255,6 +145,4 @@ DatetimeWidgetComponent.defaultProps = { resettable: true, }; -export default injectLazyLibs(['reactDates', 'moment'])( - DatetimeWidgetComponent, -); +export default DatetimeWidgetComponent; diff --git a/packages/volto/src/components/manage/Widgets/DatetimeWidget.test.jsx b/packages/volto/src/components/manage/Widgets/DatetimeWidget.test.jsx index beff9e02392..a63f03ee9e8 100644 --- a/packages/volto/src/components/manage/Widgets/DatetimeWidget.test.jsx +++ b/packages/volto/src/components/manage/Widgets/DatetimeWidget.test.jsx @@ -6,14 +6,6 @@ import { waitFor, render, screen } from '@testing-library/react'; const mockStore = configureStore(); -vi.mock('@plone/volto/helpers/Loadable/Loadable'); -beforeAll(async () => { - const { __setLoadables } = await import( - '@plone/volto/helpers/Loadable/Loadable' - ); - await __setLoadables(); -}); - test('renders a datetime widget component', async () => { const store = mockStore({ intl: { @@ -31,13 +23,11 @@ test('renders a datetime widget component', async () => { fieldSet="default" onChange={() => {}} value={isoDate} - showTime={true} /> , ); await waitFor(() => screen.getByText(/My field/)); - await waitFor(() => screen.getByPlaceholderText('Time')); expect(container).toMatchSnapshot(); }); @@ -57,12 +47,57 @@ test('datetime widget converts UTC date and adapts to local datetime', async () title="My field" onChange={() => {}} value={date} - showTime={true} /> , ); await waitFor(() => screen.getByText(/My field/)); - await waitFor(() => screen.getByPlaceholderText('Time')); expect(container).toMatchSnapshot(); }); + +test('renders a date-only widget when dateOnly is true', async () => { + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + + const { container } = render( + + {}} + value="2020-02-10" + dateOnly + /> + , + ); + + await waitFor(() => screen.getByText(/My field/)); + expect(container).toMatchSnapshot(); +}); + +test('returns null when open_end and id is end', () => { + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + + const { container } = render( + + {}} + value="2020-02-10T15:01:00.000Z" + formData={{ open_end: true }} + /> + , + ); + + expect(container.innerHTML).toBe(''); +}); diff --git a/packages/volto/src/components/manage/Widgets/TimeWidget.jsx b/packages/volto/src/components/manage/Widgets/TimeWidget.jsx index 1cfd90248b7..39c7d295aa5 100644 --- a/packages/volto/src/components/manage/Widgets/TimeWidget.jsx +++ b/packages/volto/src/components/manage/Widgets/TimeWidget.jsx @@ -1,68 +1,70 @@ -import React from 'react'; +import { useCallback } from 'react'; import PropTypes from 'prop-types'; import { defineMessages, useIntl } from 'react-intl'; -import loadable from '@loadable/component'; +import { TimeField } from '@plone/components'; +import { Time } from '@internationalized/date'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper'; -import { toBackendLang } from '@plone/volto/helpers/Utils/Utils'; -import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; import clearSVG from '@plone/volto/icons/clear.svg'; -import 'rc-time-picker/assets/index.css'; - -const TimePicker = loadable(() => import('rc-time-picker')); - const messages = defineMessages({ - time: { - id: 'Time', - defaultMessage: 'Time', + clearTime: { + id: 'Clear time', + defaultMessage: 'Clear time', }, }); -const TimeWidgetComponent = (props) => { - const { id, resettable, moment, value, onChange, isDisabled } = props; +/** + * Parse a 'HH:mm' string into a Time object. + */ +const parseTimeString = (value) => { + if (!value) return null; + const [hours, minutes] = value.split(':').map(Number); + return new Time(hours, minutes); +}; + +/** + * Format a TimeValue object back to 'HH:mm' string. + */ +const formatTimeValue = (time) => { + if (!time) return null; + return `${String(time.hour).padStart(2, '0')}:${String(time.minute).padStart(2, '0')}`; +}; + +export const TimeWidgetComponent = (props) => { + const { id, resettable, value, onChange, isDisabled } = props; const intl = useIntl(); - const lang = intl.locale; - const onTimeChange = (time) => { - if (time) { - onChange(id, time.format('HH:mm')); - } - }; + const onTimeChange = useCallback( + (time) => { + if (time) { + onChange(id, formatTimeValue(time)); + } + }, + [id, onChange], + ); - const onResetTime = () => { + const onResetTime = useCallback(() => { onChange(id, null); - }; + }, [id, onChange]); return (
-
- -
+ {resettable && ( @@ -87,10 +89,8 @@ TimeWidgetComponent.defaultProps = { description: null, required: false, error: [], - dateOnly: false, - noPastDates: false, value: null, resettable: true, }; -export default injectLazyLibs(['reactDates', 'moment'])(TimeWidgetComponent); +export default TimeWidgetComponent; diff --git a/packages/volto/src/components/manage/Widgets/TimeWidget.test.jsx b/packages/volto/src/components/manage/Widgets/TimeWidget.test.jsx index 65e49ff1969..c45149fc71c 100644 --- a/packages/volto/src/components/manage/Widgets/TimeWidget.test.jsx +++ b/packages/volto/src/components/manage/Widgets/TimeWidget.test.jsx @@ -6,15 +6,29 @@ import { waitFor, render, screen } from '@testing-library/react'; const mockStore = configureStore(); -vi.mock('@plone/volto/helpers/Loadable/Loadable'); -beforeAll(async () => { - const { __setLoadables } = await import( - '@plone/volto/helpers/Loadable/Loadable' +test('renders a time widget component', async () => { + const store = mockStore({ + intl: { + locale: 'en', + messages: {}, + }, + }); + const { container } = render( + + {}} + value={'12:00'} + /> + , ); - await __setLoadables(); + await waitFor(() => screen.getByText(/My field/)); + expect(container).toMatchSnapshot(); }); -test('renders a time widget component', async () => { +test('renders a time widget with no value', async () => { const store = mockStore({ intl: { locale: 'en', @@ -28,11 +42,10 @@ test('renders a time widget component', async () => { title="My field" fieldSet="default" onChange={() => {}} - value={'12:00'} + value={null} /> , ); await waitFor(() => screen.getByText(/My field/)); - await waitFor(() => screen.getByPlaceholderText('Time')); expect(container).toMatchSnapshot(); }); diff --git a/packages/volto/src/components/manage/Widgets/__snapshots__/DatetimeWidget.test.jsx.snap b/packages/volto/src/components/manage/Widgets/__snapshots__/DatetimeWidget.test.jsx.snap index b309547973b..17457a0e8c7 100644 --- a/packages/volto/src/components/manage/Widgets/__snapshots__/DatetimeWidget.test.jsx.snap +++ b/packages/volto/src/components/manage/Widgets/__snapshots__/DatetimeWidget.test.jsx.snap @@ -32,74 +32,472 @@ exports[`datetime widget converts UTC date and adapts to local datetime 1`] = ` class="date-time-widget-wrapper" >
-
-
-
- -

- Navigate forward to interact with the calendar and select a date. Press the question mark key to get the keyboard shortcuts for changing dates. -

-
-
+ + +
+ +
+
+ + + + +`; + +exports[`renders a date-only widget when dateOnly is true 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+ - - + +
+
+
@@ -141,74 +539,282 @@ exports[`renders a datetime widget component 1`] = ` class="date-time-widget-wrapper" >
-
-
-
- -

- Navigate forward to interact with the calendar and select a date. Press the question mark key to get the keyboard shortcuts for changing dates. -

-
-
+ + +
diff --git a/packages/volto/src/components/manage/Widgets/__snapshots__/TimeWidget.test.jsx.snap b/packages/volto/src/components/manage/Widgets/__snapshots__/TimeWidget.test.jsx.snap index 7a58e2ec063..b00b8551efe 100644 --- a/packages/volto/src/components/manage/Widgets/__snapshots__/TimeWidget.test.jsx.snap +++ b/packages/volto/src/components/manage/Widgets/__snapshots__/TimeWidget.test.jsx.snap @@ -32,25 +32,315 @@ exports[`renders a time widget component 1`] = ` class="date-time-widget-wrapper" >
+
- + + 12 + + + + 00 + + + + + PM + +
+ +
+ +
+
+
+ + + +`; + +exports[`renders a time widget with no value 1`] = ` +
+
+
+
+
+
+ +
+
+
+
+
+ +
+ + + –– + + + + –– + + + + + AM + +
+