@plone/volto 18.35.0 → 18.36.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/CHANGELOG.md +58 -0
- package/locales/af/LC_MESSAGES/volto.po +42 -11
- package/locales/af.json +1 -1
- package/locales/ar/LC_MESSAGES/volto.po +42 -11
- package/locales/ar.json +1 -1
- package/locales/bg/LC_MESSAGES/volto.po +42 -11
- package/locales/bg.json +1 -1
- package/locales/bn/LC_MESSAGES/volto.po +42 -11
- package/locales/bn.json +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +42 -11
- package/locales/ca.json +1 -1
- package/locales/cs/LC_MESSAGES/volto.po +42 -11
- package/locales/cs.json +1 -1
- package/locales/cy/LC_MESSAGES/volto.po +42 -11
- package/locales/cy.json +1 -1
- package/locales/da/LC_MESSAGES/volto.po +42 -11
- package/locales/da.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +42 -11
- package/locales/de.json +1 -1
- package/locales/el/LC_MESSAGES/volto.po +42 -11
- package/locales/el.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +42 -11
- package/locales/en.json +1 -1
- package/locales/en_AU/LC_MESSAGES/volto.po +42 -11
- package/locales/en_AU.json +1 -1
- package/locales/en_GB/LC_MESSAGES/volto.po +42 -11
- package/locales/en_GB.json +1 -1
- package/locales/eo/LC_MESSAGES/volto.po +42 -11
- package/locales/eo.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +42 -11
- package/locales/es.json +1 -1
- package/locales/et/LC_MESSAGES/volto.po +42 -11
- package/locales/et.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +42 -11
- package/locales/eu.json +1 -1
- package/locales/fa/LC_MESSAGES/volto.po +42 -11
- package/locales/fa.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +42 -11
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +42 -11
- package/locales/fr.json +1 -1
- package/locales/fu/LC_MESSAGES/volto.po +42 -11
- package/locales/fu.json +1 -1
- package/locales/gl/LC_MESSAGES/volto.po +42 -11
- package/locales/gl.json +1 -1
- package/locales/he/LC_MESSAGES/volto.po +42 -11
- package/locales/he.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +42 -11
- package/locales/hi.json +1 -1
- package/locales/hr/LC_MESSAGES/volto.po +42 -11
- package/locales/hr.json +1 -1
- package/locales/hu/LC_MESSAGES/volto.po +42 -11
- package/locales/hu.json +1 -1
- package/locales/hy/LC_MESSAGES/volto.po +42 -11
- package/locales/hy.json +1 -1
- package/locales/id/LC_MESSAGES/volto.po +42 -11
- package/locales/id.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +42 -11
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +42 -11
- package/locales/ja.json +1 -1
- package/locales/ka/LC_MESSAGES/volto.po +42 -11
- package/locales/ka.json +1 -1
- package/locales/kn/LC_MESSAGES/volto.po +42 -11
- package/locales/kn.json +1 -1
- package/locales/ko/LC_MESSAGES/volto.po +42 -11
- package/locales/ko.json +1 -1
- package/locales/lt/LC_MESSAGES/volto.po +42 -11
- package/locales/lt.json +1 -1
- package/locales/lv/LC_MESSAGES/volto.po +42 -11
- package/locales/lv.json +1 -1
- package/locales/mi/LC_MESSAGES/volto.po +42 -11
- package/locales/mi.json +1 -1
- package/locales/mk/LC_MESSAGES/volto.po +42 -11
- package/locales/mk.json +1 -1
- package/locales/my/LC_MESSAGES/volto.po +42 -11
- package/locales/my.json +1 -1
- package/locales/nb_NO/LC_MESSAGES/volto.po +42 -11
- package/locales/nb_NO.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +42 -11
- package/locales/nl.json +1 -1
- package/locales/nn/LC_MESSAGES/volto.po +42 -11
- package/locales/nn.json +1 -1
- package/locales/pl/LC_MESSAGES/volto.po +42 -11
- package/locales/pl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +42 -11
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +42 -11
- package/locales/pt_BR.json +1 -1
- package/locales/rm/LC_MESSAGES/volto.po +42 -11
- package/locales/rm.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +42 -11
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +42 -11
- package/locales/ru.json +1 -1
- package/locales/sk/LC_MESSAGES/volto.po +42 -11
- package/locales/sk.json +1 -1
- package/locales/sl/LC_MESSAGES/volto.po +42 -11
- package/locales/sl.json +1 -1
- package/locales/sm/LC_MESSAGES/volto.po +42 -11
- package/locales/sm.json +1 -1
- package/locales/sq/LC_MESSAGES/volto.po +42 -11
- package/locales/sq.json +1 -1
- package/locales/sr/LC_MESSAGES/volto.po +42 -11
- package/locales/sr.json +1 -1
- package/locales/sr@cyrl/LC_MESSAGES/volto.po +42 -11
- package/locales/sr@cyrl.json +1 -1
- package/locales/sr@latn/LC_MESSAGES/volto.po +42 -11
- package/locales/sr@latn.json +1 -1
- package/locales/sv/LC_MESSAGES/volto.po +42 -11
- package/locales/sv.json +1 -1
- package/locales/ta/LC_MESSAGES/volto.po +42 -11
- package/locales/ta.json +1 -1
- package/locales/te/LC_MESSAGES/volto.po +42 -11
- package/locales/te.json +1 -1
- package/locales/th/LC_MESSAGES/volto.po +42 -11
- package/locales/th.json +1 -1
- package/locales/to/LC_MESSAGES/volto.po +42 -11
- package/locales/to.json +1 -1
- package/locales/tr/LC_MESSAGES/volto.po +42 -11
- package/locales/tr.json +1 -1
- package/locales/uk/LC_MESSAGES/volto.po +42 -11
- package/locales/uk.json +1 -1
- package/locales/vi/LC_MESSAGES/volto.po +42 -11
- package/locales/vi.json +1 -1
- package/locales/volto.pot +40 -9
- package/locales/zh_CN/LC_MESSAGES/volto.po +42 -11
- package/locales/zh_CN.json +1 -1
- package/locales/zh_Hant/LC_MESSAGES/volto.po +42 -11
- package/locales/zh_Hant.json +1 -1
- package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +42 -11
- package/locales/zh_Hant_HK.json +1 -1
- package/package.json +18 -22
- package/src/actions/querystringsearch/querystringsearch.js +4 -1
- package/src/actions/querystringsearch/querystringsearch.test.js +77 -0
- package/src/components/manage/BlockChooser/BlockChooser.jsx +7 -10
- package/src/components/manage/Blocks/Block/Edit.jsx +9 -10
- package/src/components/manage/Blocks/Block/Order/Item.jsx +9 -4
- package/src/components/manage/Blocks/Image/View.test.jsx +8 -8
- package/src/components/manage/Contents/Contents.jsx +3 -3
- package/src/components/manage/Contents/ContentsBreadcrumbs.jsx +7 -6
- package/src/components/manage/Contents/ContentsItem.jsx +1 -3
- package/src/components/manage/Contents/DropZoneContent.jsx +1 -0
- package/src/components/manage/Sidebar/ObjectBrowser.jsx +7 -0
- package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +30 -5
- package/src/components/manage/Sidebar/ObjectBrowserBody.test.jsx +52 -0
- package/src/components/manage/Toolbar/More.jsx +13 -0
- package/src/components/manage/Toolbar/Toolbar.jsx +91 -8
- package/src/components/manage/Widgets/ArrayWidget.jsx +3 -2
- package/src/components/manage/Widgets/FileWidget.jsx +71 -17
- package/src/components/manage/Widgets/FormFieldWrapper.jsx +16 -3
- package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +1 -0
- package/src/components/manage/Widgets/QuerystringWidget.jsx +1 -18
- package/src/components/manage/Widgets/QuerystringWidget.test.jsx +45 -2
- package/src/components/manage/Widgets/RegistryImageWidget.jsx +4 -2
- package/src/components/manage/Widgets/SelectStyling.jsx +33 -1
- package/src/components/manage/Widgets/SelectWidget.jsx +3 -2
- package/src/components/manage/Widgets/TextWidget.test.jsx +44 -0
- package/src/components/theme/Sitemap/Sitemap.jsx +21 -28
- package/src/components/theme/Sitemap/Sitemap.test.jsx +18 -0
- package/src/components/theme/Unauthorized/Unauthorized.jsx +1 -2
- package/src/components/theme/View/EventView.stories.jsx +89 -0
- package/src/components/theme/View/FileView.stories.jsx +50 -0
- package/src/components/theme/View/LinkView.stories.jsx +57 -0
- package/src/components/theme/View/ListingView.stories.jsx +70 -0
- package/src/components/theme/View/NewsItemView.stories.jsx +58 -0
- package/src/components/theme/View/RenderBlocks.stories.jsx +112 -0
- package/src/components/theme/View/SummaryView.stories.jsx +71 -0
- package/src/components/theme/View/TabularView.stories.jsx +66 -0
- package/src/config/index.js +1 -0
- package/src/helpers/I18n/I18n.test.ts +44 -0
- package/src/helpers/I18n/I18n.ts +31 -0
- package/src/helpers/index.js +1 -0
- package/src/server.jsx +7 -1
- package/theme/themes/pastanaga/extras/focus.less +33 -0
- package/theme/themes/pastanaga/extras/main.less +2 -0
- package/theme/themes/pastanaga/extras/skiplinks.less +0 -2
- package/theme/themes/pastanaga/globals/site.variables +7 -0
- package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
- package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
- package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
- package/types/components/manage/Sidebar/ObjectBrowserBody.test.d.ts +1 -0
- package/types/components/manage/Widgets/ImageWidget.d.ts +1 -1
- package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
- package/types/components/manage/Widgets/QuerystringWidget.d.ts +0 -4
- package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
- package/types/components/manage/Widgets/index.d.ts +2 -2
- package/types/components/theme/View/EventView.stories.d.ts +19 -0
- package/types/components/theme/View/FileView.stories.d.ts +18 -0
- package/types/components/theme/View/LinkView.stories.d.ts +18 -0
- package/types/components/theme/View/ListingView.stories.d.ts +24 -0
- package/types/components/theme/View/NewsItemView.stories.d.ts +23 -0
- package/types/components/theme/View/RenderBlocks.stories.d.ts +23 -0
- package/types/components/theme/View/SummaryView.stories.d.ts +23 -0
- package/types/components/theme/View/TabularView.stories.d.ts +23 -0
- package/types/helpers/I18n/I18n.d.ts +20 -0
- package/types/helpers/index.d.ts +1 -0
|
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|
|
4
4
|
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
|
5
5
|
import { Link, useHistory } from 'react-router-dom';
|
|
6
6
|
import find from 'lodash/find';
|
|
7
|
+
import { Dimmer, Loader } from 'semantic-ui-react';
|
|
7
8
|
import { toast } from 'react-toastify';
|
|
8
9
|
|
|
9
10
|
import Toast from '@plone/volto/components/manage/Toast/Toast';
|
|
@@ -103,6 +104,10 @@ const messages = defineMessages({
|
|
|
103
104
|
id: 'workingCopyGenericError',
|
|
104
105
|
defaultMessage: 'An error occurred while performing this operation.',
|
|
105
106
|
},
|
|
107
|
+
Loading: {
|
|
108
|
+
id: 'Loading',
|
|
109
|
+
defaultMessage: 'Loading',
|
|
110
|
+
},
|
|
106
111
|
});
|
|
107
112
|
|
|
108
113
|
const More = (props) => {
|
|
@@ -120,6 +125,8 @@ const More = (props) => {
|
|
|
120
125
|
const workingCopyApply = workingCopy?.apply.loading;
|
|
121
126
|
const workingCopyCreate = workingCopy?.create.loading;
|
|
122
127
|
const workingCopyRemove = workingCopy?.remove.loading;
|
|
128
|
+
const isWorkingCopyLoading =
|
|
129
|
+
workingCopyApply || workingCopyCreate || workingCopyRemove;
|
|
123
130
|
|
|
124
131
|
const prevWorkingCopyApplyLoading = usePrevious(workingCopyApply);
|
|
125
132
|
const prevWorkingCopyCreateLoading = usePrevious(workingCopyCreate);
|
|
@@ -213,6 +220,9 @@ const More = (props) => {
|
|
|
213
220
|
: null,
|
|
214
221
|
}}
|
|
215
222
|
>
|
|
223
|
+
<Dimmer active={isWorkingCopyLoading} inverted>
|
|
224
|
+
<Loader>{intl.formatMessage(messages.Loading)}</Loader>
|
|
225
|
+
</Dimmer>
|
|
216
226
|
<header>
|
|
217
227
|
<h2>{content.title}</h2>
|
|
218
228
|
<button
|
|
@@ -332,6 +342,7 @@ const More = (props) => {
|
|
|
332
342
|
<li>
|
|
333
343
|
<button
|
|
334
344
|
aria-label={intl.formatMessage(messages.CreateWorkingCopy)}
|
|
345
|
+
disabled={workingCopyCreate}
|
|
335
346
|
onClick={() => {
|
|
336
347
|
dispatch(createWorkingCopy(path)).then((response) => {
|
|
337
348
|
history.push(flattenToAppURL(response['@id']));
|
|
@@ -351,6 +362,7 @@ const More = (props) => {
|
|
|
351
362
|
<li>
|
|
352
363
|
<button
|
|
353
364
|
aria-label={intl.formatMessage(messages.applyWorkingCopy)}
|
|
365
|
+
disabled={workingCopyApply}
|
|
354
366
|
onClick={() => {
|
|
355
367
|
dispatch(applyWorkingCopy(path)).then((response) => {
|
|
356
368
|
history.push(flattenToAppURL(content.working_copy_of['@id']));
|
|
@@ -392,6 +404,7 @@ const More = (props) => {
|
|
|
392
404
|
<li>
|
|
393
405
|
<button
|
|
394
406
|
aria-label={intl.formatMessage(messages.removeWorkingCopy)}
|
|
407
|
+
disabled={workingCopyRemove}
|
|
395
408
|
onClick={() => {
|
|
396
409
|
dispatch(removeWorkingCopy(path)).then((response) => {
|
|
397
410
|
history.push(flattenToAppURL(content.working_copy_of['@id']));
|
|
@@ -108,6 +108,18 @@ const messages = defineMessages({
|
|
|
108
108
|
id: 'Unlock',
|
|
109
109
|
defaultMessage: 'Unlock',
|
|
110
110
|
},
|
|
111
|
+
menuOpened: {
|
|
112
|
+
id: 'Menu opened',
|
|
113
|
+
defaultMessage: 'Menu opened',
|
|
114
|
+
},
|
|
115
|
+
menuClosed: {
|
|
116
|
+
id: 'Menu closed',
|
|
117
|
+
defaultMessage: 'Menu closed',
|
|
118
|
+
},
|
|
119
|
+
focusOn: {
|
|
120
|
+
id: 'Focus on',
|
|
121
|
+
defaultMessage: 'Focus on',
|
|
122
|
+
},
|
|
111
123
|
});
|
|
112
124
|
|
|
113
125
|
let toolbarComponents = {
|
|
@@ -185,6 +197,7 @@ class Toolbar extends Component {
|
|
|
185
197
|
toolbarRef = React.createRef();
|
|
186
198
|
toolbarWindow = React.createRef();
|
|
187
199
|
buttonRef = React.createRef();
|
|
200
|
+
announceRef = React.createRef();
|
|
188
201
|
|
|
189
202
|
constructor(props) {
|
|
190
203
|
super(props);
|
|
@@ -305,10 +318,39 @@ class Toolbar extends Component {
|
|
|
305
318
|
}
|
|
306
319
|
// PersonalTools always shows at bottom
|
|
307
320
|
if (selector === 'personalTools') {
|
|
308
|
-
this.setState(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
321
|
+
this.setState(
|
|
322
|
+
(state) => ({
|
|
323
|
+
showMenu: !state.showMenu,
|
|
324
|
+
menuStyle: { bottom: 0 },
|
|
325
|
+
}),
|
|
326
|
+
() => {
|
|
327
|
+
// Scoped only to personalTools — does not affect other toolbar flows
|
|
328
|
+
const candidates =
|
|
329
|
+
this.toolbarWindow.current?.querySelectorAll(
|
|
330
|
+
'a, button, input, [tabindex]:not([tabindex="-1"])',
|
|
331
|
+
) ?? [];
|
|
332
|
+
const firstVisible = Array.from(candidates).find((el) => {
|
|
333
|
+
const style = window.getComputedStyle(el);
|
|
334
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
335
|
+
});
|
|
336
|
+
firstVisible?.focus();
|
|
337
|
+
|
|
338
|
+
if (this.announceRef.current) {
|
|
339
|
+
const focusedLabel =
|
|
340
|
+
firstVisible?.getAttribute('aria-label') ||
|
|
341
|
+
firstVisible?.textContent?.trim() ||
|
|
342
|
+
'';
|
|
343
|
+
this.announceRef.current.textContent = '';
|
|
344
|
+
setTimeout(() => {
|
|
345
|
+
if (this.announceRef.current) {
|
|
346
|
+
this.announceRef.current.textContent = focusedLabel
|
|
347
|
+
? `${this.props.intl.formatMessage(messages.menuOpened)}, ${this.props.intl.formatMessage(messages.focusOn)} ${focusedLabel}`
|
|
348
|
+
: this.props.intl.formatMessage(messages.menuOpened);
|
|
349
|
+
}
|
|
350
|
+
}, 100);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
);
|
|
312
354
|
} else if (selector === 'more') {
|
|
313
355
|
this.setState((state) => ({
|
|
314
356
|
showMenu: !state.showMenu,
|
|
@@ -337,10 +379,21 @@ class Toolbar extends Component {
|
|
|
337
379
|
|
|
338
380
|
handleClickOutside = (e) => {
|
|
339
381
|
const target = e.target;
|
|
340
|
-
if (this.pusher && doesNodeContainClick(this.pusher, e))
|
|
382
|
+
if (this.pusher && doesNodeContainClick(this.pusher, e)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
this.toolbarRef.current &&
|
|
388
|
+
doesNodeContainClick(this.toolbarRef.current, e)
|
|
389
|
+
) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (target.closest('.ui.modal') || target.closest('.ui.dimmer')) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
341
396
|
|
|
342
|
-
// if the click is on the same button, do not close the menu as it
|
|
343
|
-
// may be handled by the toggleMenu action
|
|
344
397
|
const button =
|
|
345
398
|
doesNodeContainClick(this.toolbarRef.current, e) &&
|
|
346
399
|
this.findAncestor(target, 'button');
|
|
@@ -376,12 +429,42 @@ class Toolbar extends Component {
|
|
|
376
429
|
<BodyClass
|
|
377
430
|
className={expanded ? 'has-toolbar' : 'has-toolbar-collapsed'}
|
|
378
431
|
/>
|
|
432
|
+
<span
|
|
433
|
+
aria-live="assertive"
|
|
434
|
+
aria-atomic="true"
|
|
435
|
+
role="status"
|
|
436
|
+
className="visually-hidden-volto"
|
|
437
|
+
ref={this.announceRef}
|
|
438
|
+
/>
|
|
379
439
|
<div
|
|
380
440
|
style={this.state.menuStyle}
|
|
381
441
|
className={
|
|
382
442
|
this.state.showMenu ? 'toolbar-content show' : 'toolbar-content'
|
|
383
443
|
}
|
|
384
444
|
ref={this.toolbarWindow}
|
|
445
|
+
onBlur={(e) => {
|
|
446
|
+
if (
|
|
447
|
+
e.relatedTarget &&
|
|
448
|
+
!this.toolbarWindow.current?.contains(e.relatedTarget)
|
|
449
|
+
) {
|
|
450
|
+
this.toolbarRef.current
|
|
451
|
+
?.querySelector('button.toolbar-handler-button')
|
|
452
|
+
?.focus();
|
|
453
|
+
|
|
454
|
+
this.closeMenu();
|
|
455
|
+
|
|
456
|
+
if (this.announceRef.current) {
|
|
457
|
+
this.announceRef.current.textContent = '';
|
|
458
|
+
// Timeout to allow the screen reader to pick up the change in content after the menu is closed
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
if (this.announceRef.current) {
|
|
461
|
+
this.announceRef.current.textContent =
|
|
462
|
+
this.props.intl.formatMessage(messages.menuClosed);
|
|
463
|
+
}
|
|
464
|
+
}, 100);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}}
|
|
385
468
|
>
|
|
386
469
|
{this.state.showMenu && (
|
|
387
470
|
// This sets the scroll locker in the body tag in mobile
|
|
@@ -630,7 +713,7 @@ class Toolbar extends Component {
|
|
|
630
713
|
aria-expanded={expanded}
|
|
631
714
|
aria-controls="toolbar-body"
|
|
632
715
|
>
|
|
633
|
-
<span aria-live="assertive" className="visually-hidden">
|
|
716
|
+
<span aria-live="assertive" className="visually-hidden-volto">
|
|
634
717
|
{expanded
|
|
635
718
|
? this.props.intl.formatMessage(messages.shrinkToolbar)
|
|
636
719
|
: this.props.intl.formatMessage(messages.expandToolbar)}
|
|
@@ -315,7 +315,8 @@ class ArrayWidget extends Component {
|
|
|
315
315
|
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
|
|
316
316
|
getHelperDimensions={({ node }) => node.getBoundingClientRect()}
|
|
317
317
|
id={`field-${this.props.id}`}
|
|
318
|
-
|
|
318
|
+
fieldTitle={this.props.title}
|
|
319
|
+
aria-label={this.props.title || undefined}
|
|
319
320
|
key={this.props.id}
|
|
320
321
|
isDisabled={this.props.disabled || this.props.isDisabled}
|
|
321
322
|
className="react-select-container"
|
|
@@ -384,7 +385,7 @@ class ArrayWidget extends Component {
|
|
|
384
385
|
)
|
|
385
386
|
)
|
|
386
387
|
}
|
|
387
|
-
isClearable
|
|
388
|
+
isClearable={!this.props.required}
|
|
388
389
|
isMulti
|
|
389
390
|
/>
|
|
390
391
|
</FormFieldWrapper>
|
|
@@ -36,11 +36,13 @@ const messages = defineMessages({
|
|
|
36
36
|
},
|
|
37
37
|
editFile: {
|
|
38
38
|
id: 'Drop file here to replace the existing file',
|
|
39
|
-
defaultMessage:
|
|
39
|
+
defaultMessage:
|
|
40
|
+
'File upload area. Press Enter or click to replace the existing file',
|
|
40
41
|
},
|
|
41
42
|
fileDrag: {
|
|
42
43
|
id: 'Drop file here to upload a new file',
|
|
43
|
-
defaultMessage:
|
|
44
|
+
defaultMessage:
|
|
45
|
+
'File upload area. Press Enter or click to open the file browser',
|
|
44
46
|
},
|
|
45
47
|
replaceFile: {
|
|
46
48
|
id: 'Replace existing file',
|
|
@@ -61,7 +63,21 @@ const messages = defineMessages({
|
|
|
61
63
|
},
|
|
62
64
|
dragAndDropActionA11y: {
|
|
63
65
|
id: 'Press Enter to browse files from your computer.',
|
|
64
|
-
defaultMessage:
|
|
66
|
+
defaultMessage:
|
|
67
|
+
'File upload area. Press Enter or click to open the file browser',
|
|
68
|
+
},
|
|
69
|
+
dragAndDropReplaceA11y: {
|
|
70
|
+
id: 'File upload area. Press Enter or click to replace the existing file',
|
|
71
|
+
defaultMessage:
|
|
72
|
+
'File upload area. Press Enter or click to replace the existing file',
|
|
73
|
+
},
|
|
74
|
+
requiredField: {
|
|
75
|
+
id: 'This field is required.',
|
|
76
|
+
defaultMessage: 'This field is required.',
|
|
77
|
+
},
|
|
78
|
+
downloadFile: {
|
|
79
|
+
id: 'Download {filename}',
|
|
80
|
+
defaultMessage: 'Download {filename}',
|
|
65
81
|
},
|
|
66
82
|
});
|
|
67
83
|
|
|
@@ -89,7 +105,7 @@ const messages = defineMessages({
|
|
|
89
105
|
*
|
|
90
106
|
*/
|
|
91
107
|
const FileWidget = (props) => {
|
|
92
|
-
const { id, value, onChange, isDisabled } = props;
|
|
108
|
+
const { id, value, onChange, isDisabled, fieldSet } = props;
|
|
93
109
|
const [fileType, setFileType] = React.useState(false);
|
|
94
110
|
const intl = useIntl();
|
|
95
111
|
|
|
@@ -171,6 +187,18 @@ const FileWidget = (props) => {
|
|
|
171
187
|
reader.readAsDataURL(files[0]);
|
|
172
188
|
};
|
|
173
189
|
|
|
190
|
+
const statusTextA11y = [
|
|
191
|
+
value
|
|
192
|
+
? intl.formatMessage(messages.dragAndDropReplaceA11y)
|
|
193
|
+
: intl.formatMessage(messages.dragAndDropActionA11y),
|
|
194
|
+
props.required && intl.formatMessage(messages.requiredField),
|
|
195
|
+
value?.filename,
|
|
196
|
+
]
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.join('. ');
|
|
199
|
+
|
|
200
|
+
const errorTextA11y = props.error?.length ? props.error.join('. ') : null;
|
|
201
|
+
|
|
174
202
|
return (
|
|
175
203
|
<FormFieldWrapper {...props}>
|
|
176
204
|
<Dropzone
|
|
@@ -179,7 +207,13 @@ const FileWidget = (props) => {
|
|
|
179
207
|
{...(props.accept ? { accept: props.accept } : {})}
|
|
180
208
|
>
|
|
181
209
|
{({ getRootProps, getInputProps, isDragActive }) => (
|
|
182
|
-
<div
|
|
210
|
+
<div
|
|
211
|
+
className="file-widget-dropzone"
|
|
212
|
+
role="button"
|
|
213
|
+
aria-labelledby={`fieldset-${fieldSet}-field-label-${id}`}
|
|
214
|
+
aria-describedby={`field-${id}-status`}
|
|
215
|
+
{...getRootProps()}
|
|
216
|
+
>
|
|
183
217
|
{isDragActive && <Dimmer active></Dimmer>}
|
|
184
218
|
{fileType ? (
|
|
185
219
|
<Image
|
|
@@ -205,34 +239,52 @@ const FileWidget = (props) => {
|
|
|
205
239
|
</div>
|
|
206
240
|
)}
|
|
207
241
|
|
|
208
|
-
|
|
242
|
+
{/* aria-hidden: keyboard access is handled by the parent div (role="button").
|
|
243
|
+
The label is a visual affordance only. The stopPropagation prevents the Dropzone
|
|
244
|
+
from opening the file dialog twice on click. */}
|
|
245
|
+
<label
|
|
246
|
+
className="label-file-widget-input"
|
|
247
|
+
htmlFor={`field-${id}`}
|
|
248
|
+
aria-hidden="true"
|
|
249
|
+
onClick={(e) => e.stopPropagation()}
|
|
250
|
+
>
|
|
209
251
|
{value
|
|
210
252
|
? intl.formatMessage(messages.replaceFile)
|
|
211
253
|
: intl.formatMessage(messages.addNewFile)}
|
|
212
|
-
<span className="visually-hidden">
|
|
213
|
-
{intl.formatMessage(messages.dragAndDropActionA11y)}
|
|
214
|
-
</span>
|
|
215
254
|
</label>
|
|
255
|
+
<span id={`field-${id}-status`} className="visually-hidden-volto">
|
|
256
|
+
{statusTextA11y}
|
|
257
|
+
</span>
|
|
258
|
+
{errorTextA11y && (
|
|
259
|
+
<span role="alert" className="visually-hidden-volto">
|
|
260
|
+
{errorTextA11y}
|
|
261
|
+
</span>
|
|
262
|
+
)}
|
|
216
263
|
<input
|
|
217
264
|
{...getInputProps({
|
|
218
265
|
type: 'file',
|
|
219
266
|
style: { display: 'none' },
|
|
220
267
|
})}
|
|
221
268
|
id={`field-${id}`}
|
|
269
|
+
{...(props.required && { 'aria-required': true })}
|
|
270
|
+
{...(props.error?.length > 0 && { 'aria-invalid': true })}
|
|
222
271
|
name={id}
|
|
223
|
-
type="file"
|
|
224
272
|
disabled={isDisabled}
|
|
225
273
|
/>
|
|
226
274
|
</div>
|
|
227
275
|
)}
|
|
228
276
|
</Dropzone>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<UniversalLink
|
|
277
|
+
{value && (
|
|
278
|
+
<div className="field-file-name">
|
|
279
|
+
<UniversalLink
|
|
280
|
+
href={value.download}
|
|
281
|
+
aria-label={intl.formatMessage(messages.downloadFile, {
|
|
282
|
+
filename: value.filename,
|
|
283
|
+
})}
|
|
284
|
+
download={true}
|
|
285
|
+
>
|
|
232
286
|
{value.filename}
|
|
233
287
|
</UniversalLink>
|
|
234
|
-
)}
|
|
235
|
-
{value && (
|
|
236
288
|
<Button
|
|
237
289
|
type="button"
|
|
238
290
|
icon
|
|
@@ -247,8 +299,8 @@ const FileWidget = (props) => {
|
|
|
247
299
|
>
|
|
248
300
|
<Icon name={deleteSVG} size="20px" />
|
|
249
301
|
</Button>
|
|
250
|
-
|
|
251
|
-
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
252
304
|
</FormFieldWrapper>
|
|
253
305
|
);
|
|
254
306
|
};
|
|
@@ -263,6 +315,7 @@ FileWidget.propTypes = {
|
|
|
263
315
|
title: PropTypes.string.isRequired,
|
|
264
316
|
description: PropTypes.string,
|
|
265
317
|
required: PropTypes.bool,
|
|
318
|
+
fieldSet: PropTypes.string,
|
|
266
319
|
error: PropTypes.arrayOf(PropTypes.string),
|
|
267
320
|
value: PropTypes.shape({
|
|
268
321
|
'@type': PropTypes.string,
|
|
@@ -280,6 +333,7 @@ FileWidget.propTypes = {
|
|
|
280
333
|
FileWidget.defaultProps = {
|
|
281
334
|
description: null,
|
|
282
335
|
required: false,
|
|
336
|
+
fieldSet: null,
|
|
283
337
|
error: [],
|
|
284
338
|
value: null,
|
|
285
339
|
};
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
* FormFieldWrapper component.
|
|
3
3
|
* @module components/manage/Widgets/FormFieldWrapper
|
|
4
4
|
*/
|
|
5
|
-
import React, {
|
|
5
|
+
import React, {
|
|
6
|
+
Children,
|
|
7
|
+
Component,
|
|
8
|
+
isValidElement,
|
|
9
|
+
cloneElement,
|
|
10
|
+
} from 'react';
|
|
6
11
|
import PropTypes from 'prop-types';
|
|
7
|
-
import { Form, Grid, Icon as IconOld, Label } from 'semantic-ui-react';
|
|
12
|
+
import { Form, Grid, Icon as IconOld, Input, Label } from 'semantic-ui-react';
|
|
8
13
|
import map from 'lodash/map';
|
|
9
14
|
import cx from 'classnames';
|
|
10
15
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
@@ -103,7 +108,15 @@ class FormFieldWrapper extends Component {
|
|
|
103
108
|
|
|
104
109
|
const wdg = (
|
|
105
110
|
<>
|
|
106
|
-
{this.props.children
|
|
111
|
+
{Children.map(this.props.children, (child) => {
|
|
112
|
+
if (isValidElement(child) && required && child.type === Input) {
|
|
113
|
+
return cloneElement(child, {
|
|
114
|
+
'aria-required': true,
|
|
115
|
+
'aria-invalid': !!(error && error.length > 0),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return child;
|
|
119
|
+
})}
|
|
107
120
|
|
|
108
121
|
<div aria-live="polite" aria-atomic="true">
|
|
109
122
|
{map(error, (message) => (
|
|
@@ -307,6 +307,7 @@ export class ObjectBrowserWidgetComponent extends Component {
|
|
|
307
307
|
this.props.openObjectBrowser({
|
|
308
308
|
mode: this.props.mode,
|
|
309
309
|
currentPath: this.props.initialPath || this.props.location.pathname,
|
|
310
|
+
initialPath: this.props.initialPath,
|
|
310
311
|
propDataName: 'value',
|
|
311
312
|
onSelectItem: (url, item) => {
|
|
312
313
|
this.onChange(item);
|
|
@@ -7,10 +7,6 @@ const messages = defineMessages({
|
|
|
7
7
|
id: 'Criteria',
|
|
8
8
|
defaultMessage: 'Criteria',
|
|
9
9
|
},
|
|
10
|
-
depth: {
|
|
11
|
-
id: 'Depth',
|
|
12
|
-
defaultMessage: 'Depth',
|
|
13
|
-
},
|
|
14
10
|
SortOn: {
|
|
15
11
|
id: 'Sort on',
|
|
16
12
|
defaultMessage: 'Sort on',
|
|
@@ -38,16 +34,7 @@ export const objectSchema = ({ intl, isDisabled, value }) => ({
|
|
|
38
34
|
{
|
|
39
35
|
id: 'default',
|
|
40
36
|
title: 'Default',
|
|
41
|
-
fields: [
|
|
42
|
-
'query',
|
|
43
|
-
...(value?.query?.filter((q) => q.i === 'path').length > 0
|
|
44
|
-
? ['depth']
|
|
45
|
-
: []),
|
|
46
|
-
'sort_on',
|
|
47
|
-
'sort_order_boolean',
|
|
48
|
-
'limit',
|
|
49
|
-
'b_size',
|
|
50
|
-
],
|
|
37
|
+
fields: ['query', 'sort_on', 'sort_order_boolean', 'limit', 'b_size'],
|
|
51
38
|
},
|
|
52
39
|
],
|
|
53
40
|
properties: {
|
|
@@ -55,10 +42,6 @@ export const objectSchema = ({ intl, isDisabled, value }) => ({
|
|
|
55
42
|
title: intl.formatMessage(messages.Criteria),
|
|
56
43
|
widget: 'query',
|
|
57
44
|
},
|
|
58
|
-
depth: {
|
|
59
|
-
title: intl.formatMessage(messages.depth),
|
|
60
|
-
type: 'number',
|
|
61
|
-
},
|
|
62
45
|
sort_on: {
|
|
63
46
|
title: intl.formatMessage(messages.SortOn),
|
|
64
47
|
widget: 'query_sort_on',
|
|
@@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store';
|
|
|
4
4
|
import { Provider } from 'react-intl-redux';
|
|
5
5
|
import { waitFor } from '@testing-library/react';
|
|
6
6
|
|
|
7
|
-
import QuerystringWidget from './QuerystringWidget';
|
|
7
|
+
import QuerystringWidget, { objectSchema } from './QuerystringWidget';
|
|
8
8
|
|
|
9
9
|
const mockStore = configureStore();
|
|
10
10
|
|
|
@@ -25,6 +25,47 @@ test('renders an querystring widget component', async () => {
|
|
|
25
25
|
expect(component.toJSON()).toMatchSnapshot();
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
describe('objectSchema', () => {
|
|
29
|
+
const mockIntl = { formatMessage: ({ defaultMessage }) => defaultMessage };
|
|
30
|
+
|
|
31
|
+
it('does not include depth in the default fieldset', () => {
|
|
32
|
+
const schema = objectSchema({ intl: mockIntl });
|
|
33
|
+
const fields = schema.fieldsets[0].fields;
|
|
34
|
+
|
|
35
|
+
expect(fields).not.toContain('depth');
|
|
36
|
+
expect(fields).toEqual([
|
|
37
|
+
'query',
|
|
38
|
+
'sort_on',
|
|
39
|
+
'sort_order_boolean',
|
|
40
|
+
'limit',
|
|
41
|
+
'b_size',
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not include depth in the schema properties', () => {
|
|
46
|
+
const schema = objectSchema({ intl: mockIntl });
|
|
47
|
+
|
|
48
|
+
expect(schema.properties).not.toHaveProperty('depth');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not include depth even when value contains a path criterion', () => {
|
|
52
|
+
const value = {
|
|
53
|
+
query: [
|
|
54
|
+
{
|
|
55
|
+
i: 'path',
|
|
56
|
+
o: 'plone.app.querystring.operation.string.path',
|
|
57
|
+
v: '/folder',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
const schema = objectSchema({ intl: mockIntl, value });
|
|
62
|
+
const fields = schema.fieldsets[0].fields;
|
|
63
|
+
|
|
64
|
+
expect(fields).not.toContain('depth');
|
|
65
|
+
expect(schema.properties).not.toHaveProperty('depth');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
28
69
|
test('can take a schemaEnhancer', async () => {
|
|
29
70
|
const store = mockStore({
|
|
30
71
|
querystring: { indexes: {} },
|
|
@@ -51,6 +92,8 @@ test('can take a schemaEnhancer', async () => {
|
|
|
51
92
|
/>
|
|
52
93
|
</Provider>,
|
|
53
94
|
);
|
|
54
|
-
await waitFor(() => {
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(component.toJSON()?.children).toHaveLength(3);
|
|
97
|
+
});
|
|
55
98
|
expect(component.toJSON()).toMatchSnapshot();
|
|
56
99
|
});
|
|
@@ -34,11 +34,13 @@ const messages = defineMessages({
|
|
|
34
34
|
},
|
|
35
35
|
editFile: {
|
|
36
36
|
id: 'Drop file here to replace the existing file',
|
|
37
|
-
defaultMessage:
|
|
37
|
+
defaultMessage:
|
|
38
|
+
'File upload area. Press Enter or click to replace the existing file',
|
|
38
39
|
},
|
|
39
40
|
fileDrag: {
|
|
40
41
|
id: 'Drop file here to upload a new file',
|
|
41
|
-
defaultMessage:
|
|
42
|
+
defaultMessage:
|
|
43
|
+
'File upload area. Press Enter or click to open the file browser',
|
|
42
44
|
},
|
|
43
45
|
replaceFile: {
|
|
44
46
|
id: 'Replace existing file',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
2
3
|
import { Popup } from 'semantic-ui-react';
|
|
3
4
|
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
4
5
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
@@ -10,6 +11,13 @@ import checkSVG from '@plone/volto/icons/check.svg';
|
|
|
10
11
|
import checkBlankSVG from '@plone/volto/icons/check-blank.svg';
|
|
11
12
|
import clearSVG from '@plone/volto/icons/clear.svg';
|
|
12
13
|
|
|
14
|
+
const messages = defineMessages({
|
|
15
|
+
clearSelection: {
|
|
16
|
+
id: 'Clear selection',
|
|
17
|
+
defaultMessage: 'Clear selection',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
13
21
|
export const MenuList = ({ children }) => {
|
|
14
22
|
return <DynamicHeightList>{children}</DynamicHeightList>;
|
|
15
23
|
};
|
|
@@ -86,8 +94,32 @@ export const DropdownIndicator = injectLazyLibs('reactSelect')((props) => {
|
|
|
86
94
|
|
|
87
95
|
export const ClearIndicator = injectLazyLibs('reactSelect')((props) => {
|
|
88
96
|
const { ClearIndicator } = props.reactSelect.components;
|
|
97
|
+
const intl = useIntl();
|
|
98
|
+
const fieldLabelId = props.selectProps?.['aria-labelledby'];
|
|
99
|
+
const clearLabelId = `${props.selectProps?.inputId}-clear-label`;
|
|
89
100
|
return (
|
|
90
|
-
<ClearIndicator
|
|
101
|
+
<ClearIndicator
|
|
102
|
+
{...props}
|
|
103
|
+
innerProps={{
|
|
104
|
+
...props.innerProps,
|
|
105
|
+
'aria-hidden': false,
|
|
106
|
+
...(fieldLabelId
|
|
107
|
+
? { 'aria-labelledby': `${fieldLabelId} ${clearLabelId}` }
|
|
108
|
+
: { 'aria-label': intl.formatMessage(messages.clearSelection) }),
|
|
109
|
+
role: 'button',
|
|
110
|
+
tabIndex: 0,
|
|
111
|
+
onKeyDown: (e) => {
|
|
112
|
+
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
props.clearValue();
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<span id={clearLabelId} hidden>
|
|
121
|
+
{intl.formatMessage(messages.clearSelection)}
|
|
122
|
+
</span>
|
|
91
123
|
<Icon name={clearSVG} size="18px" color="#e40166" />
|
|
92
124
|
</ClearIndicator>
|
|
93
125
|
);
|
|
@@ -263,9 +263,10 @@ class SelectWidget extends Component {
|
|
|
263
263
|
<FormFieldWrapper {...this.props}>
|
|
264
264
|
<Select
|
|
265
265
|
id={`field-${id}`}
|
|
266
|
+
fieldTitle={this.props.title}
|
|
266
267
|
key={choices}
|
|
267
268
|
name={id}
|
|
268
|
-
aria-
|
|
269
|
+
aria-label={this.props.title || undefined}
|
|
269
270
|
menuShouldScrollIntoView={false}
|
|
270
271
|
isDisabled={disabled}
|
|
271
272
|
isSearchable={true}
|
|
@@ -304,7 +305,7 @@ class SelectWidget extends Component {
|
|
|
304
305
|
: undefined,
|
|
305
306
|
);
|
|
306
307
|
}}
|
|
307
|
-
isClearable={this.props.isClearable}
|
|
308
|
+
isClearable={!this.props.required && this.props.isClearable}
|
|
308
309
|
/>
|
|
309
310
|
</FormFieldWrapper>
|
|
310
311
|
);
|