@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.
Files changed (197) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/locales/af/LC_MESSAGES/volto.po +42 -11
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +42 -11
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +42 -11
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +42 -11
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +42 -11
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +42 -11
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +42 -11
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +42 -11
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +42 -11
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +42 -11
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +42 -11
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +42 -11
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +42 -11
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +42 -11
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +42 -11
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +42 -11
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +42 -11
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +42 -11
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +42 -11
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +42 -11
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +42 -11
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +42 -11
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +42 -11
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +42 -11
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +42 -11
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +42 -11
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +42 -11
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +42 -11
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +42 -11
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +42 -11
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +42 -11
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +42 -11
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +42 -11
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +42 -11
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +42 -11
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +42 -11
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +42 -11
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +42 -11
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +42 -11
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +42 -11
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +42 -11
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +42 -11
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +42 -11
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +42 -11
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +42 -11
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +42 -11
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +42 -11
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +42 -11
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +42 -11
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +42 -11
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +42 -11
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +42 -11
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +42 -11
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +42 -11
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +42 -11
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +42 -11
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +42 -11
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +42 -11
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +42 -11
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +42 -11
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +42 -11
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +42 -11
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +40 -9
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +42 -11
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +42 -11
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +42 -11
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +18 -22
  134. package/src/actions/querystringsearch/querystringsearch.js +4 -1
  135. package/src/actions/querystringsearch/querystringsearch.test.js +77 -0
  136. package/src/components/manage/BlockChooser/BlockChooser.jsx +7 -10
  137. package/src/components/manage/Blocks/Block/Edit.jsx +9 -10
  138. package/src/components/manage/Blocks/Block/Order/Item.jsx +9 -4
  139. package/src/components/manage/Blocks/Image/View.test.jsx +8 -8
  140. package/src/components/manage/Contents/Contents.jsx +3 -3
  141. package/src/components/manage/Contents/ContentsBreadcrumbs.jsx +7 -6
  142. package/src/components/manage/Contents/ContentsItem.jsx +1 -3
  143. package/src/components/manage/Contents/DropZoneContent.jsx +1 -0
  144. package/src/components/manage/Sidebar/ObjectBrowser.jsx +7 -0
  145. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +30 -5
  146. package/src/components/manage/Sidebar/ObjectBrowserBody.test.jsx +52 -0
  147. package/src/components/manage/Toolbar/More.jsx +13 -0
  148. package/src/components/manage/Toolbar/Toolbar.jsx +91 -8
  149. package/src/components/manage/Widgets/ArrayWidget.jsx +3 -2
  150. package/src/components/manage/Widgets/FileWidget.jsx +71 -17
  151. package/src/components/manage/Widgets/FormFieldWrapper.jsx +16 -3
  152. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +1 -0
  153. package/src/components/manage/Widgets/QuerystringWidget.jsx +1 -18
  154. package/src/components/manage/Widgets/QuerystringWidget.test.jsx +45 -2
  155. package/src/components/manage/Widgets/RegistryImageWidget.jsx +4 -2
  156. package/src/components/manage/Widgets/SelectStyling.jsx +33 -1
  157. package/src/components/manage/Widgets/SelectWidget.jsx +3 -2
  158. package/src/components/manage/Widgets/TextWidget.test.jsx +44 -0
  159. package/src/components/theme/Sitemap/Sitemap.jsx +21 -28
  160. package/src/components/theme/Sitemap/Sitemap.test.jsx +18 -0
  161. package/src/components/theme/Unauthorized/Unauthorized.jsx +1 -2
  162. package/src/components/theme/View/EventView.stories.jsx +89 -0
  163. package/src/components/theme/View/FileView.stories.jsx +50 -0
  164. package/src/components/theme/View/LinkView.stories.jsx +57 -0
  165. package/src/components/theme/View/ListingView.stories.jsx +70 -0
  166. package/src/components/theme/View/NewsItemView.stories.jsx +58 -0
  167. package/src/components/theme/View/RenderBlocks.stories.jsx +112 -0
  168. package/src/components/theme/View/SummaryView.stories.jsx +71 -0
  169. package/src/components/theme/View/TabularView.stories.jsx +66 -0
  170. package/src/config/index.js +1 -0
  171. package/src/helpers/I18n/I18n.test.ts +44 -0
  172. package/src/helpers/I18n/I18n.ts +31 -0
  173. package/src/helpers/index.js +1 -0
  174. package/src/server.jsx +7 -1
  175. package/theme/themes/pastanaga/extras/focus.less +33 -0
  176. package/theme/themes/pastanaga/extras/main.less +2 -0
  177. package/theme/themes/pastanaga/extras/skiplinks.less +0 -2
  178. package/theme/themes/pastanaga/globals/site.variables +7 -0
  179. package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
  180. package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
  181. package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
  182. package/types/components/manage/Sidebar/ObjectBrowserBody.test.d.ts +1 -0
  183. package/types/components/manage/Widgets/ImageWidget.d.ts +1 -1
  184. package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
  185. package/types/components/manage/Widgets/QuerystringWidget.d.ts +0 -4
  186. package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
  187. package/types/components/manage/Widgets/index.d.ts +2 -2
  188. package/types/components/theme/View/EventView.stories.d.ts +19 -0
  189. package/types/components/theme/View/FileView.stories.d.ts +18 -0
  190. package/types/components/theme/View/LinkView.stories.d.ts +18 -0
  191. package/types/components/theme/View/ListingView.stories.d.ts +24 -0
  192. package/types/components/theme/View/NewsItemView.stories.d.ts +23 -0
  193. package/types/components/theme/View/RenderBlocks.stories.d.ts +23 -0
  194. package/types/components/theme/View/SummaryView.stories.d.ts +23 -0
  195. package/types/components/theme/View/TabularView.stories.d.ts +23 -0
  196. package/types/helpers/I18n/I18n.d.ts +20 -0
  197. 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((state) => ({
309
- showMenu: !state.showMenu,
310
- menuStyle: { bottom: 0 },
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)) return;
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
- aria-labelledby={`fieldset-${this.props.fieldSet}-field-label-${this.props.id}`}
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: 'Drop file here to replace the existing file',
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: 'Drop file here to upload a new file',
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: 'Press Enter to browse files from your computer.',
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 className="file-widget-dropzone" {...getRootProps()}>
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
- <label className="label-file-widget-input">
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
- <div className="field-file-name">
230
- {value && (
231
- <UniversalLink href={value.download} download={true}>
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
- </div>
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, { Component } from '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: 'Drop file here to replace the existing file',
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: 'Drop file here to upload a new file',
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 {...props}>
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-labelledby={`fieldset-${this.props.fieldSet}-field-label-${id}`}
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
  );