@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 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.37",
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-dev-utils": "1.0.0-alpha.3"
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 className="container">
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
- moment.locale(toBackendLang(lang));
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
- moment.locale(toBackendLang(lang));
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
  ''
@@ -3,7 +3,7 @@ import cx from 'classnames';
3
3
 
4
4
  const RichTextWidget = ({ value, className }) =>
5
5
  value ? (
6
- <p
6
+ <div
7
7
  className={cx(className, 'richtext', 'widget')}
8
8
  dangerouslySetInnerHTML={{
9
9
  __html: value.data,
@@ -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, see https://tc39.es/ecma402/#datetimeformat-objects
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', () => {