@plone/volto 19.0.0-alpha.37 → 19.0.0-alpha.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/package.json +5 -5
- package/src/components/theme/Search/Search.jsx +15 -2
- package/src/components/theme/Widgets/DateWidget.jsx +4 -5
- package/src/components/theme/Widgets/DatetimeWidget.jsx +4 -5
- package/src/components/theme/Widgets/RichTextWidget.jsx +1 -1
- package/src/helpers/Utils/Date.js +26 -1
- package/src/helpers/Utils/Date.test.js +237 -0
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,24 @@ myst:
|
|
|
17
17
|
|
|
18
18
|
<!-- towncrier release notes start -->
|
|
19
19
|
|
|
20
|
+
## 19.0.0-alpha.38 (2026-05-19)
|
|
21
|
+
|
|
22
|
+
### Breaking
|
|
23
|
+
|
|
24
|
+
- The `RichTextWidget` wraps its value in a `div` instead of a `p`. @nileshgulia1 [#7950](https://github.com/plone/volto/issues/7950)
|
|
25
|
+
|
|
26
|
+
### Feature
|
|
27
|
+
|
|
28
|
+
- Replace moment.js with native Intl formatting in DateWidget and DatetimeWidget theme display widgets. The `formatDate` helper now accepts moment-style token strings ('ll', 'lll', 'LLLL', 'L', 'LT') for backward compatibility. @avoinea [#6732](https://github.com/plone/volto/issues/6732)
|
|
29
|
+
|
|
30
|
+
### Bugfix
|
|
31
|
+
|
|
32
|
+
- fixed a11y in search page. @giuliaghisini [#8085](https://github.com/plone/volto/issues/8085)
|
|
33
|
+
|
|
34
|
+
### Documentation
|
|
35
|
+
|
|
36
|
+
- Add Python 3.14 to Plone 6.2 support for Volto 19. @stevepiercy [#8051](https://github.com/plone/volto/issues/8051)
|
|
37
|
+
|
|
20
38
|
## 19.0.0-alpha.37 (2026-05-19)
|
|
21
39
|
|
|
22
40
|
### Feature
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "19.0.0-alpha.
|
|
12
|
+
"version": "19.0.0-alpha.38",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git@github.com:plone/volto.git"
|
|
@@ -181,8 +181,8 @@
|
|
|
181
181
|
"use-deep-compare-effect": "1.8.1",
|
|
182
182
|
"uuid": "^14.0.0",
|
|
183
183
|
"@plone/components": "4.0.0-alpha.8",
|
|
184
|
-
"@plone/scripts": "4.0.0-alpha.8",
|
|
185
184
|
"@plone/registry": "3.0.0-alpha.12",
|
|
185
|
+
"@plone/scripts": "4.0.0-alpha.8",
|
|
186
186
|
"@plone/volto-slate": "19.0.0-alpha.19"
|
|
187
187
|
},
|
|
188
188
|
"devDependencies": {
|
|
@@ -289,11 +289,11 @@
|
|
|
289
289
|
"webpack-bundle-analyzer": "4.10.1",
|
|
290
290
|
"webpack-dev-server": "^5.2.4",
|
|
291
291
|
"webpack-node-externals": "3.0.0",
|
|
292
|
-
"@plone/razzle": "1.0.0-alpha.5",
|
|
293
|
-
"@plone/types": "2.0.0-alpha.20",
|
|
294
292
|
"@plone/babel-preset-razzle": "^1.0.0-alpha.1",
|
|
293
|
+
"@plone/razzle-dev-utils": "1.0.0-alpha.3",
|
|
295
294
|
"@plone/volto-coresandbox": "1.0.0",
|
|
296
|
-
"@plone/razzle
|
|
295
|
+
"@plone/razzle": "1.0.0-alpha.5",
|
|
296
|
+
"@plone/types": "2.0.0-alpha.21"
|
|
297
297
|
},
|
|
298
298
|
"scripts": {
|
|
299
299
|
"analyze": "BUNDLE_ANALYZE=true razzle build",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
1
|
+
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
2
2
|
import { useSelector, useDispatch } from 'react-redux';
|
|
3
3
|
import { useLocation, useHistory } from 'react-router-dom';
|
|
4
4
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
|
@@ -77,12 +77,25 @@ const Search = (props) => {
|
|
|
77
77
|
[location.search, history],
|
|
78
78
|
);
|
|
79
79
|
|
|
80
|
+
// Focus search results after they render
|
|
81
|
+
const resultsRef = useRef();
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
resultsRef.current?.focus();
|
|
84
|
+
}, [items]);
|
|
85
|
+
|
|
80
86
|
const options = qs.parse(location.search);
|
|
81
87
|
|
|
82
88
|
return (
|
|
83
89
|
<Container id="page-search">
|
|
84
90
|
<Helmet title={intl.formatMessage(messages.Search)} />
|
|
85
|
-
<div
|
|
91
|
+
<div
|
|
92
|
+
className="container"
|
|
93
|
+
role="region"
|
|
94
|
+
aria-live="polite"
|
|
95
|
+
id="search-results"
|
|
96
|
+
tabIndex={-1}
|
|
97
|
+
ref={resultsRef}
|
|
98
|
+
>
|
|
86
99
|
<article id="content">
|
|
87
100
|
<header>
|
|
88
101
|
<h1 className="documentFirstHeading">
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import moment from 'moment';
|
|
4
3
|
import { useSelector } from 'react-redux';
|
|
5
4
|
import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
|
|
5
|
+
import { formatDate } from '@plone/volto/helpers/Utils/Date';
|
|
6
6
|
|
|
7
7
|
const DateWidget = ({ value, children, className, format = 'll' }) => {
|
|
8
8
|
const lang = useSelector((state) => state.intl.locale);
|
|
9
|
-
|
|
9
|
+
const locale = toBackendLang(lang);
|
|
10
|
+
const formatted = formatDate({ date: value, format, locale });
|
|
10
11
|
return value ? (
|
|
11
12
|
<span className={cx(className, 'date', 'widget')}>
|
|
12
|
-
{children
|
|
13
|
-
? children(moment(value).format(format))
|
|
14
|
-
: moment(value).format(format)}
|
|
13
|
+
{children ? children(formatted) : formatted}
|
|
15
14
|
</span>
|
|
16
15
|
) : (
|
|
17
16
|
''
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import moment from 'moment';
|
|
4
3
|
import { useSelector } from 'react-redux';
|
|
5
4
|
import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
|
|
5
|
+
import { formatDate } from '@plone/volto/helpers/Utils/Date';
|
|
6
6
|
|
|
7
7
|
const DatetimeWidget = ({ value, children, className, format = 'lll' }) => {
|
|
8
8
|
const lang = useSelector((state) => state.intl.locale);
|
|
9
|
-
|
|
9
|
+
const locale = toBackendLang(lang);
|
|
10
|
+
const formatted = formatDate({ date: value, format, locale });
|
|
10
11
|
return value ? (
|
|
11
12
|
<span className={cx(className, 'datetime', 'widget')}>
|
|
12
|
-
{children
|
|
13
|
-
? children(moment(value).format(format))
|
|
14
|
-
: moment(value).format(format)}
|
|
13
|
+
{children ? children(formatted) : formatted}
|
|
15
14
|
</span>
|
|
16
15
|
) : (
|
|
17
16
|
''
|
|
@@ -5,6 +5,21 @@ const DAY = HOUR * 24;
|
|
|
5
5
|
const MONTH = DAY * 30;
|
|
6
6
|
const YEAR = DAY * 365; // ? is this safe or should it be more accurate
|
|
7
7
|
|
|
8
|
+
// moment-style format tokens → Intl.DateTimeFormat options (for backward compat)
|
|
9
|
+
const MOMENT_FORMAT_MAP = {
|
|
10
|
+
ll: { year: 'numeric', month: 'short', day: 'numeric' },
|
|
11
|
+
lll: {
|
|
12
|
+
year: 'numeric',
|
|
13
|
+
month: 'short',
|
|
14
|
+
day: 'numeric',
|
|
15
|
+
hour: 'numeric',
|
|
16
|
+
minute: 'numeric',
|
|
17
|
+
},
|
|
18
|
+
LLLL: { dateStyle: 'full', timeStyle: 'short' },
|
|
19
|
+
L: { year: 'numeric', month: '2-digit', day: '2-digit' },
|
|
20
|
+
LT: { timeStyle: 'short' },
|
|
21
|
+
};
|
|
22
|
+
|
|
8
23
|
export const short_date_format = {
|
|
9
24
|
// 12/9/2021
|
|
10
25
|
year: 'numeric',
|
|
@@ -32,13 +47,23 @@ export const toDate = (d) =>
|
|
|
32
47
|
*/
|
|
33
48
|
export function formatDate({
|
|
34
49
|
date, // Date() or '2022-01-03T19:26:08.999Z'
|
|
35
|
-
format, // format object,
|
|
50
|
+
format, // format object (Intl.DateTimeFormat options) or moment-style token string (e.g. 'll', 'lll', 'LLLL', 'L', 'LT')
|
|
36
51
|
locale = 'en',
|
|
37
52
|
long, // true if format should be in long readable form.
|
|
38
53
|
includeTime, // true if short date format should include time
|
|
39
54
|
formatToParts = false,
|
|
40
55
|
}) {
|
|
41
56
|
date = toDate(date);
|
|
57
|
+
// Resolve moment-style token strings to Intl format options
|
|
58
|
+
if (typeof format === 'string') {
|
|
59
|
+
if (process.env.NODE_ENV !== 'production' && !MOMENT_FORMAT_MAP[format]) {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.warn(
|
|
62
|
+
`[formatDate] Unknown format token "${format}". Supported tokens: ${Object.keys(MOMENT_FORMAT_MAP).join(', ')}. Falling back to short_date_format.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
format = MOMENT_FORMAT_MAP[format] || short_date_format;
|
|
66
|
+
}
|
|
42
67
|
format = format
|
|
43
68
|
? format
|
|
44
69
|
: long && !includeTime
|
|
@@ -79,6 +79,243 @@ describe('formatDate helper', () => {
|
|
|
79
79
|
}),
|
|
80
80
|
).toBe('J 03, 2022');
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
describe('moment-style format tokens', () => {
|
|
84
|
+
it('formats with "ll" token (short date)', () => {
|
|
85
|
+
expect(formatDate({ date, format: 'll' })).toBe('Jan 3, 2022');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('formats with "lll" token (short date + time)', () => {
|
|
89
|
+
expect(formatDate({ date, format: 'lll' })).toBe('Jan 3, 2022, 7:26 PM');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('formats with "LLLL" token (full date + time)', () => {
|
|
93
|
+
expect(formatDate({ date, format: 'LLLL' })).toBe(
|
|
94
|
+
'Monday, January 3, 2022 at 7:26 PM',
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('formats with "L" token (numeric short date)', () => {
|
|
99
|
+
expect(formatDate({ date, format: 'L' })).toBe('01/03/2022');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('formats with "LT" token (time only)', () => {
|
|
103
|
+
expect(formatDate({ date, format: 'LT' })).toBe('7:26 PM');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('moment token works with other locales', () => {
|
|
107
|
+
expect(formatDate({ date, format: 'll', locale: 'de' })).toBe(
|
|
108
|
+
'3. Jan. 2022',
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('unknown string format falls back to short_date_format', () => {
|
|
113
|
+
expect(formatDate({ date, format: 'unknown' })).toBe('1/3/2022');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Italian locale (it)', () => {
|
|
118
|
+
it('formats a basic date', () => {
|
|
119
|
+
expect(formatDate({ date, locale: 'it' })).toBe('03/01/2022');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('formats a date with time', () => {
|
|
123
|
+
expect(formatDate({ date, locale: 'it', includeTime: true })).toBe(
|
|
124
|
+
'03/01/22, 19:26',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('formats a date as long', () => {
|
|
129
|
+
expect(formatDate({ date, locale: 'it', long: true })).toBe(
|
|
130
|
+
'lunedì 3 gennaio 2022 alle ore 19:26',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('formats with "ll" token', () => {
|
|
135
|
+
expect(formatDate({ date, format: 'll', locale: 'it' })).toBe(
|
|
136
|
+
'3 gen 2022',
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('formats with "lll" token', () => {
|
|
141
|
+
expect(formatDate({ date, format: 'lll', locale: 'it' })).toBe(
|
|
142
|
+
'3 gen 2022, 19:26',
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('formats with "LLLL" token', () => {
|
|
147
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'it' })).toBe(
|
|
148
|
+
'lunedì 3 gennaio 2022 alle ore 19:26',
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('formats with "L" token', () => {
|
|
153
|
+
expect(formatDate({ date, format: 'L', locale: 'it' })).toBe(
|
|
154
|
+
'03/01/2022',
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('formats with "LT" token', () => {
|
|
159
|
+
expect(formatDate({ date, format: 'LT', locale: 'it' })).toBe('19:26');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('German locale (de)', () => {
|
|
164
|
+
it('formats a basic date', () => {
|
|
165
|
+
expect(formatDate({ date, locale: 'de' })).toBe('3.1.2022');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('formats a date with time', () => {
|
|
169
|
+
expect(formatDate({ date, locale: 'de', includeTime: true })).toBe(
|
|
170
|
+
'03.01.22, 19:26',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('formats a date as long', () => {
|
|
175
|
+
expect(formatDate({ date, locale: 'de', long: true })).toBe(
|
|
176
|
+
'Montag, 3. Januar 2022 um 19:26',
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('formats with "ll" token', () => {
|
|
181
|
+
expect(formatDate({ date, format: 'll', locale: 'de' })).toBe(
|
|
182
|
+
'3. Jan. 2022',
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('formats with "lll" token', () => {
|
|
187
|
+
expect(formatDate({ date, format: 'lll', locale: 'de' })).toBe(
|
|
188
|
+
'3. Jan. 2022, 19:26',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('formats with "LLLL" token', () => {
|
|
193
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'de' })).toBe(
|
|
194
|
+
'Montag, 3. Januar 2022 um 19:26',
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('formats with "L" token', () => {
|
|
199
|
+
expect(formatDate({ date, format: 'L', locale: 'de' })).toBe(
|
|
200
|
+
'03.01.2022',
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('formats with "LT" token', () => {
|
|
205
|
+
expect(formatDate({ date, format: 'LT', locale: 'de' })).toBe('19:26');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Brazilian Portuguese locale (pt-BR)', () => {
|
|
210
|
+
it('formats a basic date', () => {
|
|
211
|
+
expect(formatDate({ date, locale: 'pt-BR' })).toBe('03/01/2022');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('formats a date with time', () => {
|
|
215
|
+
expect(formatDate({ date, locale: 'pt-BR', includeTime: true })).toBe(
|
|
216
|
+
'03/01/2022, 19:26',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('formats a date as long', () => {
|
|
221
|
+
expect(formatDate({ date, locale: 'pt-BR', long: true })).toBe(
|
|
222
|
+
'segunda-feira, 3 de janeiro de 2022 às 19:26',
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('formats with "ll" token', () => {
|
|
227
|
+
expect(formatDate({ date, format: 'll', locale: 'pt-BR' })).toBe(
|
|
228
|
+
'3 de jan. de 2022',
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('formats with "lll" token', () => {
|
|
233
|
+
expect(formatDate({ date, format: 'lll', locale: 'pt-BR' })).toBe(
|
|
234
|
+
'3 de jan. de 2022, 19:26',
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('formats with "LLLL" token', () => {
|
|
239
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'pt-BR' })).toBe(
|
|
240
|
+
'segunda-feira, 3 de janeiro de 2022 às 19:26',
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('formats with "L" token', () => {
|
|
245
|
+
expect(formatDate({ date, format: 'L', locale: 'pt-BR' })).toBe(
|
|
246
|
+
'03/01/2022',
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('formats with "LT" token', () => {
|
|
251
|
+
expect(formatDate({ date, format: 'LT', locale: 'pt-BR' })).toBe('19:26');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('Romanian locale (ro)', () => {
|
|
256
|
+
it('formats a basic date', () => {
|
|
257
|
+
expect(formatDate({ date, locale: 'ro' })).toBe('03.01.2022');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('formats a date with time', () => {
|
|
261
|
+
expect(formatDate({ date, locale: 'ro', includeTime: true })).toBe(
|
|
262
|
+
'03.01.2022, 19:26',
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('formats a date as long', () => {
|
|
267
|
+
expect(formatDate({ date, locale: 'ro', long: true })).toBe(
|
|
268
|
+
'luni, 3 ianuarie 2022 la 19:26',
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('formats with "ll" token', () => {
|
|
273
|
+
expect(formatDate({ date, format: 'll', locale: 'ro' })).toBe(
|
|
274
|
+
'3 ian. 2022',
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('formats with "lll" token', () => {
|
|
279
|
+
expect(formatDate({ date, format: 'lll', locale: 'ro' })).toBe(
|
|
280
|
+
'3 ian. 2022, 19:26',
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('formats with "LLLL" token', () => {
|
|
285
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'ro' })).toBe(
|
|
286
|
+
'luni, 3 ianuarie 2022 la 19:26',
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('formats with "L" token', () => {
|
|
291
|
+
expect(formatDate({ date, format: 'L', locale: 'ro' })).toBe(
|
|
292
|
+
'03.01.2022',
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('formats with "LT" token', () => {
|
|
297
|
+
expect(formatDate({ date, format: 'LT', locale: 'ro' })).toBe('19:26');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// moment vs Intl comparison
|
|
302
|
+
// This section documents known differences between the former moment.js output
|
|
303
|
+
// and our Intl-based formatDate implementation. Reference values are hardcoded
|
|
304
|
+
// from moment@2.29.4 to avoid a runtime dependency on the library we are removing.
|
|
305
|
+
//
|
|
306
|
+
// Tokens that match moment exactly: ll, L, LT (most locales), LLLL (pt-BR only)
|
|
307
|
+
//
|
|
308
|
+
// Known differences from moment@2.29.4:
|
|
309
|
+
// - lll (all locales): Intl inserts a comma between date and time;
|
|
310
|
+
// moment did not (e.g. "Jan 3, 2022, 7:26 PM" vs "Jan 3, 2022 7:26 PM")
|
|
311
|
+
// - LLLL (en, it, de, ro): Intl inserts a locale-specific preposition before
|
|
312
|
+
// the time; moment did not
|
|
313
|
+
// en: "...at 7:26 PM" vs "...7:26 PM"
|
|
314
|
+
// it: "...alle ore 19:26" vs "...19:26"
|
|
315
|
+
// de: "...um 19:26" vs "...19:26"
|
|
316
|
+
// ro: "...la 19:26" vs "...19:26"
|
|
317
|
+
// - ll/lll (pt-BR): Intl adds a period after abbreviated month ("jan.");
|
|
318
|
+
// moment did not ("jan")
|
|
82
319
|
});
|
|
83
320
|
|
|
84
321
|
describe('formatRelativeDate helper', () => {
|