@plone/volto 18.33.1 → 18.35.0
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/.release-it.json +3 -0
- package/CHANGELOG.md +52 -0
- package/README.md +0 -1
- package/locales/af/LC_MESSAGES/volto.po +55 -0
- package/locales/af.json +1 -1
- package/locales/ar/LC_MESSAGES/volto.po +55 -0
- package/locales/ar.json +1 -1
- package/locales/bg/LC_MESSAGES/volto.po +55 -0
- package/locales/bg.json +1 -1
- package/locales/bn/LC_MESSAGES/volto.po +55 -0
- package/locales/bn.json +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +57 -2
- package/locales/ca.json +1 -1
- package/locales/cs/LC_MESSAGES/volto.po +55 -0
- package/locales/cs.json +1 -1
- package/locales/cy/LC_MESSAGES/volto.po +55 -0
- package/locales/cy.json +1 -1
- package/locales/da/LC_MESSAGES/volto.po +55 -0
- package/locales/da.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +59 -4
- package/locales/de.json +1 -1
- package/locales/el/LC_MESSAGES/volto.po +55 -0
- package/locales/el.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +55 -0
- package/locales/en.json +1 -1
- package/locales/en_AU/LC_MESSAGES/volto.po +55 -0
- package/locales/en_AU.json +1 -1
- package/locales/en_GB/LC_MESSAGES/volto.po +55 -0
- package/locales/en_GB.json +1 -1
- package/locales/eo/LC_MESSAGES/volto.po +55 -0
- package/locales/eo.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +73 -18
- package/locales/es.json +1 -1
- package/locales/et/LC_MESSAGES/volto.po +55 -0
- package/locales/et.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +58 -3
- package/locales/eu.json +1 -1
- package/locales/fa/LC_MESSAGES/volto.po +55 -0
- package/locales/fa.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +55 -0
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +56 -1
- package/locales/fr.json +1 -1
- package/locales/fu/LC_MESSAGES/volto.po +55 -0
- package/locales/fu.json +1 -1
- package/locales/gl/LC_MESSAGES/volto.po +60 -5
- package/locales/gl.json +1 -1
- package/locales/he/LC_MESSAGES/volto.po +55 -0
- package/locales/he.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +58 -3
- package/locales/hi.json +1 -1
- package/locales/hr/LC_MESSAGES/volto.po +55 -0
- package/locales/hr.json +1 -1
- package/locales/hu/LC_MESSAGES/volto.po +55 -0
- package/locales/hu.json +1 -1
- package/locales/hy/LC_MESSAGES/volto.po +55 -0
- package/locales/hy.json +1 -1
- package/locales/id/LC_MESSAGES/volto.po +55 -0
- package/locales/id.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +56 -1
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +55 -0
- package/locales/ja.json +1 -1
- package/locales/ka/LC_MESSAGES/volto.po +55 -0
- package/locales/ka.json +1 -1
- package/locales/kn/LC_MESSAGES/volto.po +55 -0
- package/locales/kn.json +1 -1
- package/locales/ko/LC_MESSAGES/volto.po +55 -0
- package/locales/ko.json +1 -1
- package/locales/lt/LC_MESSAGES/volto.po +55 -0
- package/locales/lt.json +1 -1
- package/locales/lv/LC_MESSAGES/volto.po +55 -0
- package/locales/lv.json +1 -1
- package/locales/mi/LC_MESSAGES/volto.po +55 -0
- package/locales/mi.json +1 -1
- package/locales/mk/LC_MESSAGES/volto.po +55 -0
- package/locales/mk.json +1 -1
- package/locales/my/LC_MESSAGES/volto.po +55 -0
- package/locales/my.json +1 -1
- package/locales/nb_NO/LC_MESSAGES/volto.po +55 -0
- package/locales/nb_NO.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +56 -1
- package/locales/nl.json +1 -1
- package/locales/nn/LC_MESSAGES/volto.po +55 -0
- package/locales/nn.json +1 -1
- package/locales/pl/LC_MESSAGES/volto.po +55 -0
- package/locales/pl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +55 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +69 -14
- package/locales/pt_BR.json +1 -1
- package/locales/rm/LC_MESSAGES/volto.po +55 -0
- package/locales/rm.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +56 -1
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +56 -1
- package/locales/ru.json +1 -1
- package/locales/sk/LC_MESSAGES/volto.po +55 -0
- package/locales/sk.json +1 -1
- package/locales/sl/LC_MESSAGES/volto.po +55 -0
- package/locales/sl.json +1 -1
- package/locales/sm/LC_MESSAGES/volto.po +55 -0
- package/locales/sm.json +1 -1
- package/locales/sq/LC_MESSAGES/volto.po +55 -0
- package/locales/sq.json +1 -1
- package/locales/sr/LC_MESSAGES/volto.po +55 -0
- package/locales/sr.json +1 -1
- package/locales/sr@cyrl/LC_MESSAGES/volto.po +55 -0
- package/locales/sr@cyrl.json +1 -1
- package/locales/sr@latn/LC_MESSAGES/volto.po +55 -0
- package/locales/sr@latn.json +1 -1
- package/locales/sv/LC_MESSAGES/volto.po +55 -0
- package/locales/sv.json +1 -1
- package/locales/ta/LC_MESSAGES/volto.po +56 -1
- package/locales/ta.json +1 -1
- package/locales/te/LC_MESSAGES/volto.po +55 -0
- package/locales/te.json +1 -1
- package/locales/th/LC_MESSAGES/volto.po +55 -0
- package/locales/th.json +1 -1
- package/locales/to/LC_MESSAGES/volto.po +55 -0
- package/locales/to.json +1 -1
- package/locales/tr/LC_MESSAGES/volto.po +55 -0
- package/locales/tr.json +1 -1
- package/locales/uk/LC_MESSAGES/volto.po +55 -0
- package/locales/uk.json +1 -1
- package/locales/vi/LC_MESSAGES/volto.po +55 -0
- package/locales/vi.json +1 -1
- package/locales/volto.pot +56 -1
- package/locales/zh_CN/LC_MESSAGES/volto.po +55 -0
- package/locales/zh_CN.json +1 -1
- package/locales/zh_Hant/LC_MESSAGES/volto.po +55 -0
- package/locales/zh_Hant.json +1 -1
- package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +55 -0
- package/locales/zh_Hant_HK.json +1 -1
- package/news/7308.fix +1 -0
- package/news/8084.fix +1 -0
- package/package.json +19 -19
- package/razzle.config.js +1 -0
- package/src/actions/users/users.js +2 -2
- package/src/components/manage/Blocks/Block/Order/Item.jsx +18 -10
- package/src/components/manage/Blocks/Block/Order/Item.test.jsx +90 -0
- package/src/components/manage/Controlpanels/AddonsControlpanel.jsx +7 -0
- package/src/components/manage/Controlpanels/ContentTypes.jsx +9 -2
- package/src/components/manage/Controlpanels/DatabaseInformation.jsx +9 -0
- package/src/components/manage/Controlpanels/ModerateComments.jsx +8 -0
- package/src/components/manage/Controlpanels/Users/UserGroupMembershipControlPanel.test.jsx +3 -0
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +58 -5
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.ssr.test.jsx +624 -0
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.test.jsx +8 -0
- package/src/components/manage/Form/Form.jsx +6 -1
- package/src/components/manage/Form/ModalForm.jsx +165 -87
- package/src/components/manage/Sidebar/Sidebar.jsx +1 -0
- package/src/components/manage/Toast/Toast.jsx +35 -1
- package/src/components/manage/Toast/Toast.test.jsx +8 -5
- package/src/components/manage/Widgets/DatetimeWidget.jsx +92 -58
- package/src/components/manage/Widgets/DatetimeWidget.test.jsx +55 -0
- package/src/components/manage/Widgets/FormFieldWrapper.jsx +7 -5
- package/src/components/manage/Widgets/TextWidget.jsx +4 -0
- package/src/components/manage/Widgets/UrlWidget.jsx +51 -6
- package/src/components/theme/Search/Search.jsx +24 -1
- package/src/components/theme/Unauthorized/Unauthorized.jsx +30 -22
- package/src/components/theme/Unauthorized/Unauthorized.test.jsx +28 -1
- package/src/helpers/FormValidation/validators.ts +15 -2
- package/theme/themes/default/globals/site.variables +2 -2
- package/theme/themes/pastanaga/collections/form.overrides +21 -0
- package/theme/themes/pastanaga/elements/button.overrides +30 -3
- package/theme/themes/pastanaga/extras/main.less +16 -0
- package/theme/themes/pastanaga/globals/site.variables +0 -2
- package/types/components/manage/Blocks/Block/Order/Item.test.d.ts +1 -0
- package/types/components/manage/Controlpanels/Users/UsersControlpanel.d.ts +2 -6
- package/types/components/manage/Controlpanels/Users/UsersControlpanel.ssr.test.d.ts +1 -0
- package/types/components/manage/Controlpanels/index.d.ts +1 -1
- package/webpack-plugins/webpack-less-plugin.js +1 -1
|
@@ -46,6 +46,14 @@ const messages = defineMessages({
|
|
|
46
46
|
id: 'Cancel',
|
|
47
47
|
defaultMessage: 'Cancel',
|
|
48
48
|
},
|
|
49
|
+
dialogOpened: {
|
|
50
|
+
id: 'Pop-up opened: {title}',
|
|
51
|
+
defaultMessage: 'Pop-up opened: {title}',
|
|
52
|
+
},
|
|
53
|
+
dialogClosed: {
|
|
54
|
+
id: 'Pop-up closed.',
|
|
55
|
+
defaultMessage: 'Pop-up closed.',
|
|
56
|
+
},
|
|
49
57
|
});
|
|
50
58
|
|
|
51
59
|
/**
|
|
@@ -54,6 +62,7 @@ const messages = defineMessages({
|
|
|
54
62
|
* @extends Component
|
|
55
63
|
*/
|
|
56
64
|
class ModalForm extends Component {
|
|
65
|
+
static idCounter = 0;
|
|
57
66
|
/**
|
|
58
67
|
* Property types.
|
|
59
68
|
* @property {Object} propTypes Property types.
|
|
@@ -110,17 +119,21 @@ class ModalForm extends Component {
|
|
|
110
119
|
*/
|
|
111
120
|
constructor(props) {
|
|
112
121
|
super(props);
|
|
122
|
+
this.headerId = `modal-title-${++ModalForm.idCounter}`;
|
|
113
123
|
this.state = {
|
|
114
124
|
currentTab: 0,
|
|
115
125
|
errors: {},
|
|
116
126
|
isFormPristine: true,
|
|
117
127
|
formData: props.formData,
|
|
118
128
|
};
|
|
129
|
+
this.modalRef = React.createRef();
|
|
130
|
+
this.announceRef = React.createRef();
|
|
119
131
|
this.selectTab = this.selectTab.bind(this);
|
|
120
132
|
this.onChangeField = this.onChangeField.bind(this);
|
|
121
133
|
this.onBlurField = this.onBlurField.bind(this);
|
|
122
134
|
this.onClickInput = this.onClickInput.bind(this);
|
|
123
135
|
this.onSubmit = this.onSubmit.bind(this);
|
|
136
|
+
this.onKeyDown = this.onKeyDown.bind(this);
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
/**
|
|
@@ -209,12 +222,57 @@ class ModalForm extends Component {
|
|
|
209
222
|
});
|
|
210
223
|
}
|
|
211
224
|
|
|
225
|
+
onKeyDown(event) {
|
|
226
|
+
if (event.key !== 'Tab') return;
|
|
227
|
+
const modal = document.getElementById(this.headerId)?.closest('.ui.modal');
|
|
228
|
+
if (!modal) return;
|
|
229
|
+
const focusable = modal.querySelectorAll(
|
|
230
|
+
'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])',
|
|
231
|
+
);
|
|
232
|
+
if (!focusable.length) return;
|
|
233
|
+
const first = focusable[0];
|
|
234
|
+
const last = focusable[focusable.length - 1];
|
|
235
|
+
if (event.shiftKey) {
|
|
236
|
+
if (document.activeElement === first) {
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
last.focus();
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
if (document.activeElement === last) {
|
|
242
|
+
event.preventDefault();
|
|
243
|
+
first.focus();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
212
248
|
/**
|
|
213
249
|
* Component did update lifecycle handler
|
|
214
250
|
* @param {Object} prevProps
|
|
215
251
|
* @param {Object} prevState
|
|
216
252
|
*/
|
|
217
|
-
|
|
253
|
+
componentWillUnmount() {
|
|
254
|
+
document.removeEventListener('keydown', this.onKeyDown);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
componentDidUpdate(prevProps, prevState) {
|
|
258
|
+
if (!prevProps.open && this.props.open) {
|
|
259
|
+
document.addEventListener('keydown', this.onKeyDown);
|
|
260
|
+
this.modalRef.current?.focus();
|
|
261
|
+
if (this.announceRef.current) {
|
|
262
|
+
this.announceRef.current.textContent = this.props.intl.formatMessage(
|
|
263
|
+
messages.dialogOpened,
|
|
264
|
+
{ title: this.props.title },
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (prevProps.open && !this.props.open) {
|
|
269
|
+
document.removeEventListener('keydown', this.onKeyDown);
|
|
270
|
+
if (this.announceRef.current) {
|
|
271
|
+
this.announceRef.current.textContent = this.props.intl.formatMessage(
|
|
272
|
+
messages.dialogClosed,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
218
276
|
if (this.props.onChangeFormData) {
|
|
219
277
|
if (!isEqual(prevState?.formData, this.state.formData)) {
|
|
220
278
|
this.props.onChangeFormData(this.state.formData);
|
|
@@ -259,100 +317,120 @@ class ModalForm extends Component {
|
|
|
259
317
|
|
|
260
318
|
const state_errors = keys(this.state.errors).length > 0;
|
|
261
319
|
return (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
''
|
|
320
|
+
<>
|
|
321
|
+
{/* aria-live region outside Modal so it persists through open/close cycles */}
|
|
322
|
+
<div
|
|
323
|
+
ref={this.announceRef}
|
|
324
|
+
aria-live="assertive"
|
|
325
|
+
aria-atomic="true"
|
|
326
|
+
style={{
|
|
327
|
+
position: 'absolute',
|
|
328
|
+
width: '1px',
|
|
329
|
+
height: '1px',
|
|
330
|
+
overflow: 'hidden',
|
|
331
|
+
opacity: 0,
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
<Modal
|
|
335
|
+
role="dialog"
|
|
336
|
+
dimmer={this.props.dimmer}
|
|
337
|
+
open={this.props.open}
|
|
338
|
+
className={this.props.className}
|
|
339
|
+
aria-labelledby={this.headerId}
|
|
340
|
+
aria-modal="true"
|
|
341
|
+
>
|
|
342
|
+
<Header id={this.headerId}>{this.props.title}</Header>
|
|
343
|
+
<Dimmer active={this.props.loading}>
|
|
344
|
+
<Loader>
|
|
345
|
+
{this.props.loadingMessage || (
|
|
346
|
+
<FormattedMessage id="Loading" defaultMessage="Loading." />
|
|
290
347
|
)}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
348
|
+
</Loader>
|
|
349
|
+
</Dimmer>
|
|
350
|
+
<Modal.Content scrolling>
|
|
351
|
+
{/* outline suppressed for programmatic focus via CSS :focus:not(:focus-visible) on .modal-focus-trap */}
|
|
352
|
+
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
|
353
|
+
<div ref={this.modalRef} tabIndex={-1} className="modal-focus-trap">
|
|
354
|
+
<UiForm
|
|
355
|
+
method="post"
|
|
356
|
+
onSubmit={this.onSubmit}
|
|
357
|
+
error={state_errors || Boolean(this.props.submitError)}
|
|
358
|
+
>
|
|
359
|
+
{description}
|
|
360
|
+
<Message error>
|
|
361
|
+
{state_errors ? (
|
|
362
|
+
<FormattedMessage
|
|
363
|
+
id="There were some errors."
|
|
364
|
+
defaultMessage="There were some errors."
|
|
365
|
+
/>
|
|
366
|
+
) : (
|
|
367
|
+
''
|
|
368
|
+
)}
|
|
369
|
+
<div>{this.props.submitError}</div>
|
|
370
|
+
</Message>
|
|
371
|
+
{schema.fieldsets?.length > 1 && (
|
|
372
|
+
<Menu tabular stackable>
|
|
373
|
+
{map(schema.fieldsets, (item, index) => (
|
|
374
|
+
<Menu.Item
|
|
375
|
+
name={item.id}
|
|
376
|
+
index={index}
|
|
377
|
+
key={item.id}
|
|
378
|
+
active={this.state.currentTab === index}
|
|
379
|
+
onClick={this.selectTab}
|
|
380
|
+
>
|
|
381
|
+
{item.title}
|
|
382
|
+
</Menu.Item>
|
|
383
|
+
))}
|
|
384
|
+
</Menu>
|
|
385
|
+
)}
|
|
386
|
+
{fields.map((field) => (
|
|
387
|
+
<Field
|
|
388
|
+
{...field}
|
|
389
|
+
key={field.id}
|
|
390
|
+
onBlur={this.onBlurField}
|
|
391
|
+
onClick={this.onClickInput}
|
|
392
|
+
error={this.state.errors[field.id]}
|
|
393
|
+
/>
|
|
305
394
|
))}
|
|
306
|
-
</
|
|
395
|
+
</UiForm>
|
|
396
|
+
</div>
|
|
397
|
+
</Modal.Content>
|
|
398
|
+
<Modal.Actions>
|
|
399
|
+
{onCancel && (
|
|
400
|
+
<Button
|
|
401
|
+
type="button"
|
|
402
|
+
basic
|
|
403
|
+
circular
|
|
404
|
+
secondary
|
|
405
|
+
aria-label={this.props.intl.formatMessage(messages.cancel)}
|
|
406
|
+
title={this.props.intl.formatMessage(messages.cancel)}
|
|
407
|
+
onClick={onCancel}
|
|
408
|
+
>
|
|
409
|
+
<Icon name={clearSVG} className="circled" size="30px" />
|
|
410
|
+
</Button>
|
|
307
411
|
)}
|
|
308
|
-
{fields.map((field) => (
|
|
309
|
-
<Field
|
|
310
|
-
{...field}
|
|
311
|
-
key={field.id}
|
|
312
|
-
onBlur={this.onBlurField}
|
|
313
|
-
onClick={this.onClickInput}
|
|
314
|
-
error={this.state.errors[field.id]}
|
|
315
|
-
/>
|
|
316
|
-
))}
|
|
317
|
-
</UiForm>
|
|
318
|
-
</Modal.Content>
|
|
319
|
-
<Modal.Actions>
|
|
320
|
-
<Button
|
|
321
|
-
basic
|
|
322
|
-
circular
|
|
323
|
-
primary
|
|
324
|
-
floated="right"
|
|
325
|
-
aria-label={
|
|
326
|
-
this.props.submitLabel
|
|
327
|
-
? this.props.submitLabel
|
|
328
|
-
: this.props.intl.formatMessage(messages.save)
|
|
329
|
-
}
|
|
330
|
-
title={
|
|
331
|
-
this.props.submitLabel
|
|
332
|
-
? this.props.submitLabel
|
|
333
|
-
: this.props.intl.formatMessage(messages.save)
|
|
334
|
-
}
|
|
335
|
-
onClick={this.onSubmit}
|
|
336
|
-
loading={this.props.loading}
|
|
337
|
-
>
|
|
338
|
-
<Icon name={aheadSVG} className="contents circled" size="30px" />
|
|
339
|
-
</Button>
|
|
340
|
-
{onCancel && (
|
|
341
412
|
<Button
|
|
342
|
-
type="button"
|
|
343
413
|
basic
|
|
344
414
|
circular
|
|
345
|
-
|
|
346
|
-
aria-label={
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
415
|
+
primary
|
|
416
|
+
aria-label={
|
|
417
|
+
this.props.submitLabel
|
|
418
|
+
? this.props.submitLabel
|
|
419
|
+
: this.props.intl.formatMessage(messages.save)
|
|
420
|
+
}
|
|
421
|
+
title={
|
|
422
|
+
this.props.submitLabel
|
|
423
|
+
? this.props.submitLabel
|
|
424
|
+
: this.props.intl.formatMessage(messages.save)
|
|
425
|
+
}
|
|
426
|
+
onClick={this.onSubmit}
|
|
427
|
+
loading={this.props.loading}
|
|
350
428
|
>
|
|
351
|
-
<Icon name={
|
|
429
|
+
<Icon name={aheadSVG} className="contents circled" size="30px" />
|
|
352
430
|
</Button>
|
|
353
|
-
|
|
354
|
-
</Modal
|
|
355
|
-
|
|
431
|
+
</Modal.Actions>
|
|
432
|
+
</Modal>
|
|
433
|
+
</>
|
|
356
434
|
);
|
|
357
435
|
}
|
|
358
436
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
4
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
4
5
|
|
|
5
6
|
import successSVG from '@plone/volto/icons/ready.svg';
|
|
@@ -7,7 +8,28 @@ import infoSVG from '@plone/volto/icons/info.svg';
|
|
|
7
8
|
import errorSVG from '@plone/volto/icons/error.svg';
|
|
8
9
|
import warningSVG from '@plone/volto/icons/warning.svg';
|
|
9
10
|
|
|
11
|
+
const messages = defineMessages({
|
|
12
|
+
success: {
|
|
13
|
+
id: 'toast_type_success',
|
|
14
|
+
defaultMessage: 'Success',
|
|
15
|
+
},
|
|
16
|
+
error: {
|
|
17
|
+
id: 'toast_type_error',
|
|
18
|
+
defaultMessage: 'Error',
|
|
19
|
+
},
|
|
20
|
+
warning: {
|
|
21
|
+
id: 'toast_type_warning',
|
|
22
|
+
defaultMessage: 'Warning',
|
|
23
|
+
},
|
|
24
|
+
info: {
|
|
25
|
+
id: 'toast_type_info',
|
|
26
|
+
defaultMessage: 'Information',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
10
30
|
const Toast = (props) => {
|
|
31
|
+
const intl = useIntl();
|
|
32
|
+
|
|
11
33
|
function getIcon(props) {
|
|
12
34
|
if (props.info) {
|
|
13
35
|
return infoSVG;
|
|
@@ -22,12 +44,24 @@ const Toast = (props) => {
|
|
|
22
44
|
}
|
|
23
45
|
}
|
|
24
46
|
|
|
47
|
+
function getTypeLabel(props) {
|
|
48
|
+
if (props.error) return intl.formatMessage(messages.error);
|
|
49
|
+
if (props.warning) return intl.formatMessage(messages.warning);
|
|
50
|
+
if (props.info) return intl.formatMessage(messages.info);
|
|
51
|
+
if (props.success) return intl.formatMessage(messages.success);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
const { title, content } = props;
|
|
56
|
+
const typeLabel = getTypeLabel(props);
|
|
26
57
|
|
|
27
58
|
return (
|
|
28
59
|
<>
|
|
29
|
-
<Icon name={getIcon(props)} size="18px" />
|
|
60
|
+
<Icon name={getIcon(props)} size="18px" ariaHidden={true} />
|
|
30
61
|
<div className="toast-inner-content">
|
|
62
|
+
{typeLabel && (
|
|
63
|
+
<span className="visually-hidden-volto">{typeLabel}</span>
|
|
64
|
+
)}
|
|
31
65
|
{title && <h4>{title}</h4>}
|
|
32
66
|
<div>{content}</div>
|
|
33
67
|
</div>
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import renderer from 'react-test-renderer';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
3
4
|
import Toast from './Toast';
|
|
4
5
|
|
|
5
6
|
test('renders a Toast info component', () => {
|
|
6
7
|
const component = renderer.create(
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
<IntlProvider locale="en">
|
|
9
|
+
<Toast
|
|
10
|
+
info
|
|
11
|
+
title="I'm a title"
|
|
12
|
+
content="This is the content, lorem ipsum"
|
|
13
|
+
/>
|
|
14
|
+
</IntlProvider>,
|
|
12
15
|
);
|
|
13
16
|
const json = component.toJSON();
|
|
14
17
|
expect(json).toMatchSnapshot();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { defineMessages, useIntl } from 'react-intl';
|
|
4
4
|
import loadable from '@loadable/component';
|
|
@@ -85,16 +85,22 @@ const DatetimeWidgetComponent = (props) => {
|
|
|
85
85
|
noPastDates: propNoPastDates,
|
|
86
86
|
isDisabled,
|
|
87
87
|
formData,
|
|
88
|
+
required,
|
|
88
89
|
} = props;
|
|
89
90
|
|
|
90
91
|
const intl = useIntl();
|
|
91
92
|
const lang = intl.locale;
|
|
92
93
|
|
|
94
|
+
// timeInputRef: for aria-required (rc-time-picker has no aria props)
|
|
95
|
+
const timeInputRef = useRef(null);
|
|
96
|
+
|
|
93
97
|
const [focused, setFocused] = useState(false);
|
|
94
98
|
const [isDefault, setIsDefault] = useState(false);
|
|
95
99
|
|
|
96
100
|
const { SingleDatePicker } = reactDates;
|
|
97
101
|
|
|
102
|
+
const renderWidget = !(id === 'end' && formData?.open_end);
|
|
103
|
+
|
|
98
104
|
useEffect(() => {
|
|
99
105
|
const parsedDateTime = parseDateTime(
|
|
100
106
|
toBackendLang(lang),
|
|
@@ -107,11 +113,6 @@ const DatetimeWidgetComponent = (props) => {
|
|
|
107
113
|
);
|
|
108
114
|
}, [value, lang, moment]);
|
|
109
115
|
|
|
110
|
-
// If open_end is checked and this is the end field, don't render
|
|
111
|
-
if (id === 'end' && formData?.open_end) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
116
|
const getInternalValue = () => {
|
|
116
117
|
return parseDateTime(toBackendLang(lang), value, undefined, moment.default);
|
|
117
118
|
};
|
|
@@ -165,68 +166,101 @@ const DatetimeWidgetComponent = (props) => {
|
|
|
165
166
|
const datetime = getInternalValue();
|
|
166
167
|
const isDateOnly = getDateOnly();
|
|
167
168
|
|
|
169
|
+
// aria-required for the time input (rc-time-picker is lazy-loaded,
|
|
170
|
+
// so MutationObserver is needed to catch when it mounts its input)
|
|
171
|
+
|
|
172
|
+
// rc-time-picker does not have aria props, so we need to set aria-required
|
|
173
|
+
// manually on the input element when the required prop changes
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!renderWidget || isDateOnly) return;
|
|
177
|
+
|
|
178
|
+
function applyTimeAria() {
|
|
179
|
+
const input = timeInputRef.current?.querySelector('input');
|
|
180
|
+
if (!input) return;
|
|
181
|
+
if (required) input.setAttribute('aria-required', 'true');
|
|
182
|
+
else input.removeAttribute('aria-required');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
applyTimeAria();
|
|
186
|
+
|
|
187
|
+
const observer = new MutationObserver(applyTimeAria);
|
|
188
|
+
if (timeInputRef.current) {
|
|
189
|
+
observer.observe(timeInputRef.current, {
|
|
190
|
+
childList: true,
|
|
191
|
+
subtree: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return () => observer.disconnect();
|
|
196
|
+
}, [required, isDateOnly, renderWidget]);
|
|
197
|
+
|
|
168
198
|
return (
|
|
169
199
|
<FormFieldWrapper {...props}>
|
|
170
|
-
|
|
171
|
-
<div
|
|
172
|
-
className={cx('ui input date-input', {
|
|
173
|
-
'default-date': isDefault,
|
|
174
|
-
})}
|
|
175
|
-
>
|
|
176
|
-
<SingleDatePicker
|
|
177
|
-
date={datetime}
|
|
178
|
-
disabled={isDisabled}
|
|
179
|
-
onDateChange={onDateChange}
|
|
180
|
-
focused={focused}
|
|
181
|
-
numberOfMonths={1}
|
|
182
|
-
{...(noPastDates ? {} : { isOutsideRange: () => false })}
|
|
183
|
-
onFocusChange={onFocusChange}
|
|
184
|
-
noBorder
|
|
185
|
-
displayFormat={moment.default
|
|
186
|
-
.localeData(toBackendLang(lang))
|
|
187
|
-
.longDateFormat('L')}
|
|
188
|
-
navPrev={<PrevIcon />}
|
|
189
|
-
navNext={<NextIcon />}
|
|
190
|
-
id={`${id}-date`}
|
|
191
|
-
placeholder={intl.formatMessage(messages.date)}
|
|
192
|
-
/>
|
|
193
|
-
</div>
|
|
194
|
-
{!isDateOnly && (
|
|
200
|
+
{renderWidget && (
|
|
201
|
+
<div className="date-time-widget-wrapper">
|
|
195
202
|
<div
|
|
196
|
-
className={cx('ui input
|
|
203
|
+
className={cx('ui input date-input', {
|
|
197
204
|
'default-date': isDefault,
|
|
198
205
|
})}
|
|
199
206
|
>
|
|
200
|
-
<
|
|
207
|
+
<SingleDatePicker
|
|
208
|
+
date={datetime}
|
|
201
209
|
disabled={isDisabled}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
onDateChange={onDateChange}
|
|
211
|
+
focused={focused}
|
|
212
|
+
numberOfMonths={1}
|
|
213
|
+
{...(noPastDates ? {} : { isOutsideRange: () => false })}
|
|
214
|
+
onFocusChange={onFocusChange}
|
|
215
|
+
noBorder
|
|
216
|
+
required={required}
|
|
217
|
+
displayFormat={moment.default
|
|
210
218
|
.localeData(toBackendLang(lang))
|
|
211
|
-
.longDateFormat('
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
.longDateFormat('L')}
|
|
220
|
+
navPrev={<PrevIcon />}
|
|
221
|
+
navNext={<NextIcon />}
|
|
222
|
+
id={`${id}-date`}
|
|
223
|
+
placeholder={intl.formatMessage(messages.date)}
|
|
215
224
|
/>
|
|
216
225
|
</div>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
{!isDateOnly && (
|
|
227
|
+
<div
|
|
228
|
+
ref={timeInputRef}
|
|
229
|
+
className={cx('ui input time-input', {
|
|
230
|
+
'default-date': isDefault,
|
|
231
|
+
})}
|
|
232
|
+
>
|
|
233
|
+
<TimePicker
|
|
234
|
+
disabled={isDisabled}
|
|
235
|
+
defaultValue={datetime}
|
|
236
|
+
value={datetime}
|
|
237
|
+
onChange={onTimeChange}
|
|
238
|
+
allowEmpty={false}
|
|
239
|
+
showSecond={false}
|
|
240
|
+
use12Hours={lang === 'en'}
|
|
241
|
+
id={`${id}-time`}
|
|
242
|
+
format={moment.default
|
|
243
|
+
.localeData(toBackendLang(lang))
|
|
244
|
+
.longDateFormat('LT')}
|
|
245
|
+
placeholder={intl.formatMessage(messages.time)}
|
|
246
|
+
focusOnOpen
|
|
247
|
+
placement="bottomRight"
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
{resettable && (
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
disabled={isDisabled || !datetime}
|
|
255
|
+
onClick={onResetDates}
|
|
256
|
+
className="item ui noborder button"
|
|
257
|
+
aria-label={intl.formatMessage(messages.clearDateTime)}
|
|
258
|
+
>
|
|
259
|
+
<Icon name={clearSVG} size="24px" className="close" />
|
|
260
|
+
</button>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
230
264
|
</FormFieldWrapper>
|
|
231
265
|
);
|
|
232
266
|
};
|
|
@@ -71,3 +71,58 @@ test('datetime widget converts UTC date and adapts to local datetime', async ()
|
|
|
71
71
|
await waitFor(() => screen.getByPlaceholderText('Time'));
|
|
72
72
|
expect(container).toMatchSnapshot();
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
test('applies aria-required attribute to the date input when required prop is true', async () => {
|
|
76
|
+
const store = mockStore({
|
|
77
|
+
intl: {
|
|
78
|
+
locale: 'en',
|
|
79
|
+
messages: {},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { container } = render(
|
|
84
|
+
<Provider store={store}>
|
|
85
|
+
<DatetimeWidget
|
|
86
|
+
id="required-field"
|
|
87
|
+
title="Required Field"
|
|
88
|
+
onChange={() => {}}
|
|
89
|
+
required={true}
|
|
90
|
+
/>
|
|
91
|
+
</Provider>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await waitFor(() => screen.getByPlaceholderText('Date'));
|
|
95
|
+
|
|
96
|
+
const dateInput = container.querySelector('.date-input input');
|
|
97
|
+
|
|
98
|
+
expect(dateInput).toHaveAttribute('required');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('applies aria-required attribute to the time input when required prop is true', async () => {
|
|
102
|
+
const store = mockStore({
|
|
103
|
+
intl: {
|
|
104
|
+
locale: 'en',
|
|
105
|
+
messages: {},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const { container } = render(
|
|
110
|
+
<Provider store={store}>
|
|
111
|
+
<DatetimeWidget
|
|
112
|
+
id="required-field"
|
|
113
|
+
title="Required Field"
|
|
114
|
+
onChange={() => {}}
|
|
115
|
+
required={true}
|
|
116
|
+
/>
|
|
117
|
+
</Provider>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Wait for the lazy-loaded TimePicker to be mounted in the DOM
|
|
121
|
+
await waitFor(() => screen.getByPlaceholderText('Time'));
|
|
122
|
+
|
|
123
|
+
// The rc-time-picker doesn't support aria-required natively,
|
|
124
|
+
// so we verify if our MutationObserver/useEffect successfully injected it.
|
|
125
|
+
const timeInput = container.querySelector('.time-input input');
|
|
126
|
+
|
|
127
|
+
expect(timeInput).toHaveAttribute('aria-required', 'true');
|
|
128
|
+
});
|
|
@@ -105,11 +105,13 @@ class FormFieldWrapper extends Component {
|
|
|
105
105
|
<>
|
|
106
106
|
{this.props.children}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{message}
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
<div aria-live="polite" aria-atomic="true">
|
|
109
|
+
{map(error, (message) => (
|
|
110
|
+
<Label key={message} basic color="red" className="form-error-label">
|
|
111
|
+
{message}
|
|
112
|
+
</Label>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
113
115
|
</>
|
|
114
116
|
);
|
|
115
117
|
|