diff --git a/packages/volto/news/6732.feature b/packages/volto/news/6732.feature
new file mode 100644
index 00000000000..cdd39302eee
--- /dev/null
+++ b/packages/volto/news/6732.feature
@@ -0,0 +1 @@
+Replace `moment.js` with native `Intl` formatting in `Comments` and `EventDatesInfo` components. @avoinea
diff --git a/packages/volto/src/components/theme/Comments/Comments.jsx b/packages/volto/src/components/theme/Comments/Comments.jsx
index 27df92a45dd..4525a9ebf71 100644
--- a/packages/volto/src/components/theme/Comments/Comments.jsx
+++ b/packages/volto/src/components/theme/Comments/Comments.jsx
@@ -3,10 +3,13 @@ import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { createPortal } from 'react-dom';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
-import { compose } from 'redux';
+
import { Button, Comment, Container, Icon } from 'semantic-ui-react';
-import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
+import {
+ formatDate,
+ formatRelativeDate,
+} from '@plone/volto/helpers/Utils/Date';
import {
addComment,
deleteComment,
@@ -200,8 +203,6 @@ const Comments = (props) => {
return allCommentsWithCildren;
};
- const moment = props.moment.default;
-
const allCommentsWithCildren = useMemo(
() => addRepliesAsChildrenToComments(items),
[items],
@@ -223,8 +224,17 @@ const Comments = (props) => {
{' '}
-
- {moment(comment.creation_date).fromNow()}
+
+ {formatRelativeDate({
+ date: comment.creation_date,
+ locale: intl.locale,
+ })}
@@ -375,4 +385,4 @@ Comments.propTypes = {
pathname: PropTypes.string.isRequired,
};
-export default compose(injectLazyLibs(['moment']))(Comments);
+export default Comments;
diff --git a/packages/volto/src/components/theme/Comments/Comments.test.jsx b/packages/volto/src/components/theme/Comments/Comments.test.jsx
index e9443571e12..205ebce0210 100644
--- a/packages/volto/src/components/theme/Comments/Comments.test.jsx
+++ b/packages/volto/src/components/theme/Comments/Comments.test.jsx
@@ -12,23 +12,18 @@ vi.mock('@plone/volto/components/theme/Comments/CommentEditModal', () => ({
const mockStore = configureStore();
-vi.mock('moment', () => ({
- default: vi.fn(() => ({
- format: vi.fn(() => 'Sunday, April 23, 2017 3:38 AM'),
- fromNow: vi.fn(() => 'a few seconds ago'),
- })),
-}));
-
-vi.mock('@plone/volto/helpers/Loadable/Loadable');
vi.mock('@plone/volto/components/manage/Form');
-beforeAll(async () => {
- const { __setLoadables } = await import(
- '@plone/volto/helpers/Loadable/Loadable'
- );
- await __setLoadables();
-});
describe('Comments', () => {
+ beforeEach(() => {
+ // Freeze time so formatRelativeDate produces deterministic output
+ vi.useFakeTimers({ now: new Date('2017-04-23T03:38:04Z') });
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
it('renders a comments component', () => {
const store = mockStore({
comments: {
@@ -37,7 +32,7 @@ describe('Comments', () => {
'@id': 'someurl',
comment_id: '1614094601171408',
author_name: 'admin',
- creation_date: '2017-11-06T19:36:01',
+ creation_date: '2017-04-23T03:38:00Z',
text: { data: 'Some comment' },
is_deletable: true,
is_editable: true,
diff --git a/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap b/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap
index 5e61cba9c39..5d6f5c00fb9 100644
--- a/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap
+++ b/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap
@@ -81,9 +81,9 @@ exports[`Comments > renders a comments component 1`] = `
- a few seconds ago
+ 4 seconds ago
diff --git a/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap b/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap
index 20f942f71ec..61adbe433ed 100644
--- a/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap
+++ b/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap
@@ -33,6 +33,45 @@ exports[`renders event details component with all props 1`] = `
>
When
+
+
+
+ Jun 23, 2019
+
+
+
+
+
+ 3:20 PM
+
+
+ to
+
+
+ Jun 24, 2019
+
+
+
+
+
+ 3:20 PM
+
+
+
@@ -146,6 +185,27 @@ exports[`renders event details component with only required props 1`] = `
>
When
+
+
+ Jun 23, 2019
+
+ from
+
+ 3:20 PM
+
+ to
+
+ 4:20 PM
+
+
@@ -188,6 +248,27 @@ exports[`renders event details component without links to api in the text 1`] =
>
When
+
+
+ Jun 23, 2019
+
+ from
+
+ 3:20 PM
+
+ to
+
+ 4:20 PM
+
+
diff --git a/packages/volto/src/components/theme/View/EventDatesInfo.jsx b/packages/volto/src/components/theme/View/EventDatesInfo.jsx
index 232df550f64..730a8d74ea2 100644
--- a/packages/volto/src/components/theme/View/EventDatesInfo.jsx
+++ b/packages/volto/src/components/theme/View/EventDatesInfo.jsx
@@ -3,11 +3,18 @@ import PropTypes from 'prop-types';
import { List } from 'semantic-ui-react';
import cx from 'classnames';
-import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import { useSelector } from 'react-redux';
+import { formatDate } from '@plone/volto/helpers/Utils/Date';
+/**
+ * @deprecated Use the native Date API directly. Will be removed in Volto 20.
+ */
export const datesForDisplay = (start, end, moment) => {
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'datesForDisplay is deprecated and will be removed in Volto 20. Use the native Date API directly.',
+ );
const mStart = moment(start);
const mEnd = moment(end);
if (!mStart.isValid() || !mEnd.isValid()) {
@@ -25,17 +32,30 @@ export const datesForDisplay = (start, end, moment) => {
};
};
-const When_ = ({ start, end, whole_day, open_end, moment: momentlib }) => {
- const lang = useSelector((state) => state.intl.locale);
-
- const moment = momentlib.default;
- moment.locale(toBackendLang(lang));
+const When_ = ({ start, end, whole_day, open_end }) => {
+ const locale = useSelector((state) => state.intl.locale);
- const datesInfo = datesForDisplay(start, end, moment);
- if (!datesInfo) {
- return;
+ const dStart = new Date(start);
+ const dEnd = end != null ? new Date(end) : new Date();
+ if (isNaN(dStart.getTime()) || isNaN(dEnd.getTime())) {
+ return null;
}
- // TODO I18N INTL
+ const sameDay =
+ dStart.getFullYear() === dEnd.getFullYear() &&
+ dStart.getMonth() === dEnd.getMonth() &&
+ dStart.getDate() === dEnd.getDate();
+ const sameTime =
+ sameDay &&
+ dStart.getHours() === dEnd.getHours() &&
+ dStart.getMinutes() === dEnd.getMinutes();
+ const datesInfo = {
+ sameDay,
+ sameTime,
+ startDate: formatDate({ date: dStart, format: 'll', locale }),
+ startTime: formatDate({ date: dStart, format: 'LT', locale }),
+ endDate: formatDate({ date: dEnd, format: 'll', locale }),
+ endTime: formatDate({ date: dEnd, format: 'LT', locale }),
+ };
return (
{
);
};
-export const When = injectLazyLibs(['moment'])(When_);
+export const When = When_;
When.propTypes = {
start: PropTypes.string.isRequired,
@@ -109,13 +129,8 @@ When.propTypes = {
open_end: PropTypes.bool,
};
-export const Recurrence_ = ({
- recurrence,
- start,
- moment: momentlib,
- rrule,
-}) => {
- const moment = momentlib.default;
+export const Recurrence_ = ({ recurrence, start, rrule }) => {
+ const locale = useSelector((state) => state.intl.locale);
const { RRule, rrulestr } = rrule;
if (recurrence.indexOf('DTSTART') < 0) {
var dtstart = RRule.optionsToString({
@@ -129,12 +144,11 @@ export const Recurrence_ = ({
datesForDisplay(date, undefined, moment))
- .map((date) => date.startDate)}
+ .map((date) => formatDate({ date, format: 'll', locale }))}
/>
);
};
-export const Recurrence = injectLazyLibs(['moment', 'rrule'])(Recurrence_);
+export const Recurrence = injectLazyLibs(['rrule'])(Recurrence_);
Recurrence.propTypes = {
recurrence: PropTypes.string.isRequired,
diff --git a/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx b/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx
index ac2d4e85c2c..a4952eec6cb 100644
--- a/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx
+++ b/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx
@@ -93,3 +93,24 @@ test('same day, not whole day, not open end', () => {
const json = component.toJSON();
expect(json).toMatchSnapshot();
});
+
+test('returns null for invalid start date', () => {
+ const component = renderer.create(
+
+
+ ,
+ );
+ expect(component.toJSON()).toBeNull();
+});
+
+test('uses current time when end is undefined', () => {
+ vi.useFakeTimers({ now: new Date('2019-06-23T15:20:00+00:00') });
+ const component = renderer.create(
+
+
+ ,
+ );
+ const json = component.toJSON();
+ expect(json).toMatchSnapshot();
+ vi.useRealTimers();
+});
diff --git a/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap b/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap
index 306d9a0c544..c27515d3b57 100644
--- a/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap
+++ b/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap
@@ -121,3 +121,27 @@ exports[`same day, whole day 1`] = `
`;
+
+exports[`uses current time when end is undefined 1`] = `
+
+
+ Jun 23, 2019
+
+ from
+
+ 11:55 AM
+
+ to
+
+ 3:20 PM
+
+
+`;
diff --git a/packages/volto/types/components/theme/View/EventDatesInfo.d.ts b/packages/volto/types/components/theme/View/EventDatesInfo.d.ts
index 0bfeadcd54d..b8059c3a900 100644
--- a/packages/volto/types/components/theme/View/EventDatesInfo.d.ts
+++ b/packages/volto/types/components/theme/View/EventDatesInfo.d.ts
@@ -1,3 +1,4 @@
+/** @deprecated Will be removed in Volto 20. Use the native Date API directly. */
export function datesForDisplay(start: any, end: any, moment: any): {
sameDay: any;
sameTime: any;
@@ -7,10 +8,9 @@ export function datesForDisplay(start: any, end: any, moment: any): {
endTime: any;
};
export const When: any;
-export function Recurrence_({ recurrence, start, moment: momentlib, rrule, }: {
+export function Recurrence_({ recurrence, start, rrule, }: {
recurrence: any;
start: any;
- moment: any;
rrule: any;
}): import("react/jsx-runtime").JSX.Element;
export const Recurrence: any;