@plone/volto 18.35.1 → 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 (148) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/locales/af/LC_MESSAGES/volto.po +37 -6
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +37 -6
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +37 -6
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +37 -6
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +37 -6
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +37 -6
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +37 -6
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +37 -6
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +37 -6
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +37 -6
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +37 -6
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +37 -6
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +37 -6
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +37 -6
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +37 -6
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +37 -6
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +37 -6
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +37 -6
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +37 -6
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +37 -6
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +37 -6
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +37 -6
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +37 -6
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +37 -6
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +37 -6
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +37 -6
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +37 -6
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +37 -6
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +37 -6
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +37 -6
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +37 -6
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +37 -6
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +37 -6
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +37 -6
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +37 -6
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +37 -6
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +37 -6
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +37 -6
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +37 -6
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +37 -6
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +37 -6
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +37 -6
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +37 -6
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +37 -6
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +37 -6
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +37 -6
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +37 -6
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +37 -6
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +37 -6
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +37 -6
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +37 -6
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +37 -6
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +37 -6
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +37 -6
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +37 -6
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +37 -6
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +37 -6
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +37 -6
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +37 -6
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +37 -6
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +37 -6
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +37 -6
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +35 -4
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +37 -6
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +37 -6
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +37 -6
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +3 -3
  134. package/src/components/manage/Contents/Contents.jsx +3 -3
  135. package/src/components/manage/Contents/ContentsItem.jsx +1 -3
  136. package/src/components/manage/Contents/DropZoneContent.jsx +1 -0
  137. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +23 -2
  138. package/src/components/manage/Toolbar/More.jsx +13 -0
  139. package/src/components/manage/Toolbar/Toolbar.jsx +91 -8
  140. package/src/components/manage/Widgets/FileWidget.jsx +71 -17
  141. package/src/components/manage/Widgets/RegistryImageWidget.jsx +4 -2
  142. package/src/components/theme/Sitemap/Sitemap.jsx +21 -28
  143. package/src/components/theme/Sitemap/Sitemap.test.jsx +18 -0
  144. package/src/config/index.js +1 -0
  145. package/theme/themes/pastanaga/extras/focus.less +33 -0
  146. package/theme/themes/pastanaga/extras/main.less +2 -0
  147. package/theme/themes/pastanaga/extras/skiplinks.less +0 -2
  148. package/theme/themes/pastanaga/globals/site.variables +7 -0
@@ -724,9 +724,9 @@ class Contents extends Component {
724
724
  delta,
725
725
  );
726
726
  } else {
727
- this.setState({
728
- items: move(this.state.items, itemIndex, itemIndex + delta),
729
- });
727
+ this.setState((prevState) => ({
728
+ items: move(prevState.items, itemIndex, itemIndex + delta),
729
+ }));
730
730
  }
731
731
  }
732
732
 
@@ -364,15 +364,13 @@ const DragDropConnector = (props) => {
364
364
  drop(props, monitor) {
365
365
  const id = monitor.getItem().id;
366
366
  const dragOrder = monitor.getItem().startOrder;
367
- const dropOrder = props.order;
367
+ const dropOrder = monitor.getItem().order;
368
368
 
369
369
  if (dragOrder === dropOrder) {
370
370
  return;
371
371
  }
372
372
 
373
373
  props.onOrderItem(id, dragOrder, dropOrder - dragOrder, true);
374
-
375
- monitor.getItem().order = dropOrder;
376
374
  },
377
375
  },
378
376
  (connect) => ({
@@ -113,6 +113,7 @@ const DropZoneContent = (props) => {
113
113
  };
114
114
 
115
115
  const onDrop = async (e) => {
116
+ e.preventDefault();
116
117
  if (!hasFiles(e)) {
117
118
  return;
118
119
  }
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
5
5
  import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
6
6
  import { Input, Segment, Breadcrumb } from 'semantic-ui-react';
7
7
 
8
+ import debounce from 'lodash/debounce';
8
9
  import join from 'lodash/join';
9
10
 
10
11
  // These absolute imports (without using the corresponding centralized index.js) are required
@@ -159,6 +160,22 @@ class ObjectBrowserBody extends Component {
159
160
  view: this.props.mode === 'image' ? 'icons' : 'list',
160
161
  };
161
162
  this.searchInputRef = React.createRef();
163
+ // Debounce the live-search dispatch so rapid keystrokes do not produce a
164
+ // burst of overlapping requests whose responses may resolve out of order
165
+ // (an older, less-filtered response arriving after the latest one would
166
+ // visually "reset" the list to look unfiltered).
167
+ this.debouncedSearch = debounce(this.doSearch, 300);
168
+ }
169
+
170
+ /**
171
+ * Component will unmount
172
+ * @method componentWillUnmount
173
+ * @returns {undefined}
174
+ */
175
+ componentWillUnmount() {
176
+ if (this.debouncedSearch) {
177
+ this.debouncedSearch.cancel();
178
+ }
162
179
  }
163
180
 
164
181
  /**
@@ -248,8 +265,7 @@ class ObjectBrowserBody extends Component {
248
265
  view: prevState.view === 'icons' ? 'list' : 'icons',
249
266
  }));
250
267
 
251
- onSearch = (e) => {
252
- const text = flattenToAppURL(e.target.value);
268
+ doSearch = (text) => {
253
269
  if (text.startsWith('/')) {
254
270
  this.setState({ currentFolder: text });
255
271
  this.props.searchContent(
@@ -286,6 +302,11 @@ class ObjectBrowserBody extends Component {
286
302
  }
287
303
  };
288
304
 
305
+ onSearch = (e) => {
306
+ const text = flattenToAppURL(e.target.value);
307
+ this.debouncedSearch(text);
308
+ };
309
+
289
310
  onSelectItem = (item) => {
290
311
  const url = item['@id'];
291
312
  const { block, data, mode, dataName, onChangeBlock } = this.props;
@@ -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)}
@@ -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
  };
@@ -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,12 +1,12 @@
1
1
  import { useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { compose } from 'redux';
4
- import { connect } from 'react-redux';
4
+ import { connect, useSelector } from 'react-redux';
5
5
  import { asyncConnect } from '@plone/volto/helpers/AsyncConnect';
6
+ import { expandToBackendURL } from '@plone/volto/helpers/Url/Url';
6
7
  import { defineMessages, injectIntl } from 'react-intl';
7
8
  import { Container as SemanticContainer } from 'semantic-ui-react';
8
9
  import Helmet from '@plone/volto/helpers/Helmet/Helmet';
9
- import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
10
10
  import { Link } from 'react-router-dom';
11
11
  import config from '@plone/volto/registry';
12
12
 
@@ -20,6 +20,9 @@ const messages = defineMessages({
20
20
  });
21
21
 
22
22
  export function getSitemapPath(pathname = '', lang) {
23
+ /* This function is deprecated
24
+ * We keep it for backwards compatibility.
25
+ */
23
26
  const prefix = pathname.replace(/\/sitemap$/gm, '').replace(/^\//, '');
24
27
  const path = prefix || lang || '';
25
28
  return path;
@@ -32,18 +35,12 @@ export function getSitemapPath(pathname = '', lang) {
32
35
  * @returns {JSX.Element} - Rendered component.
33
36
  */
34
37
  function Sitemap(props) {
35
- const {
36
- location: { pathname },
37
- lang,
38
- getNavigation,
39
- } = props;
40
-
38
+ const { getNavigation, navroot } = props;
41
39
  useEffect(() => {
42
40
  const { settings } = config;
43
- const language = settings.isMultilingual ? `${toBackendLang(lang)}` : null;
44
- const path = getSitemapPath(pathname, language);
45
- getNavigation(path, 4);
46
- }, [pathname, lang, getNavigation]);
41
+ const path = `${expandToBackendURL(navroot?.navroot?.['@id'])}`;
42
+ getNavigation(path, settings.siteMapDepth);
43
+ }, [navroot, getNavigation]);
47
44
 
48
45
  const renderItems = (items) => {
49
46
  return (
@@ -64,12 +61,13 @@ function Sitemap(props) {
64
61
  const Container =
65
62
  config.getComponent({ name: 'Container' }).component || SemanticContainer;
66
63
 
64
+ const items = useSelector((state) => state.navigation.items);
67
65
  return (
68
66
  <div id="page-sitemap">
69
67
  <Helmet title={props.intl.formatMessage(messages.Sitemap)} />
70
68
  <Container className="view-wrapper">
71
69
  <h1>{props.intl.formatMessage(messages.Sitemap)} </h1>
72
- {props.items && renderItems(props.items)}
70
+ {items && renderItems(items)}
73
71
  </Container>
74
72
  </div>
75
73
  );
@@ -77,18 +75,15 @@ function Sitemap(props) {
77
75
 
78
76
  Sitemap.propTypes = {
79
77
  getNavigation: PropTypes.func.isRequired,
80
- location: PropTypes.object.isRequired,
78
+ navroot: PropTypes.object.isRequired,
81
79
  intl: PropTypes.object.isRequired,
82
- lang: PropTypes.string.isRequired,
83
- items: PropTypes.array.isRequired,
84
80
  };
85
81
 
86
82
  export const __test__ = compose(
87
83
  injectIntl,
88
84
  connect(
89
85
  (state) => ({
90
- items: state.navigation.items,
91
- lang: state.intl.locale,
86
+ navroot: state.navroot?.data,
92
87
  }),
93
88
  { getNavigation },
94
89
  ),
@@ -98,24 +93,22 @@ export default compose(
98
93
  injectIntl,
99
94
  connect(
100
95
  (state) => ({
101
- items: state.navigation.items,
102
- lang: state.intl.locale,
96
+ navroot: state.navroot?.data,
103
97
  }),
104
98
  { getNavigation },
105
99
  ),
106
100
  asyncConnect([
107
101
  {
108
102
  key: 'navigation',
109
- promise: ({ location, store: { dispatch, getState } }) => {
103
+ promise: ({ store: { dispatch, getState } }) => {
110
104
  if (!__SERVER__) return;
111
- const { settings } = config;
112
- const path = getSitemapPath(
113
- location.pathname,
114
- settings.isMultilingual
115
- ? toBackendLang(getState().intl.locale)
116
- : null,
105
+ const navroot = getState().navroot.data?.navroot?.['@id'];
106
+ return dispatch(
107
+ getNavigation(
108
+ expandToBackendURL(navroot),
109
+ config.settings.siteMapDepth,
110
+ ),
117
111
  );
118
- return dispatch(getNavigation(path, 4));
119
112
  },
120
113
  },
121
114
  ]),
@@ -43,6 +43,15 @@ describe('Sitemap', () => {
43
43
  locale: 'en',
44
44
  messages: {},
45
45
  },
46
+ navroot: {
47
+ data: {
48
+ navroot: {
49
+ '@id': `http://localhost:8080/Plone/`,
50
+ '@type': 'Plone Site',
51
+ title: 'Plone Site',
52
+ },
53
+ },
54
+ },
46
55
  });
47
56
  const component = renderer.create(
48
57
  <Provider store={store}>
@@ -94,6 +103,15 @@ describe('Sitemap in a multilingual site', () => {
94
103
  locale: 'en',
95
104
  messages: {},
96
105
  },
106
+ navroot: {
107
+ data: {
108
+ navroot: {
109
+ '@id': `http://localhost:8080/Plone/en`,
110
+ '@type': 'LRF',
111
+ title: 'English',
112
+ },
113
+ },
114
+ },
97
115
  });
98
116
  const component = renderer.create(
99
117
  <Provider store={store}>
@@ -115,6 +115,7 @@ let config = {
115
115
  supportedLanguages: ['en'],
116
116
  defaultLanguage: process.env.SITE_DEFAULT_LANGUAGE || 'en',
117
117
  navDepth: 1,
118
+ siteMapDepth: 4,
118
119
  expressMiddleware: serverConfig.expressMiddleware, // BBB
119
120
  defaultBlockType: 'slate',
120
121
  verticalFormTabs: false,
@@ -0,0 +1,33 @@
1
+ .universal-focus-style() {
2
+ box-shadow: 0 0 0 2px @innerFocusShadow !important;
3
+ outline: 2px solid @outerFocusOutline !important;
4
+ outline-offset: 2px;
5
+ }
6
+
7
+ // Global focus styles for all elements that support :focus-visible
8
+ // Exclude contenteditable elements (slate editors, rich text inputs) — focus is
9
+ // already implied by the text cursor; an outline ring is noisy and confusing.
10
+ :focus-visible:not([contenteditable]):not([data-slate-editor]) {
11
+ .universal-focus-style();
12
+ }
13
+
14
+ //inputs modifications
15
+ input:focus-visible {
16
+ border-color: @outerFocusOutline !important;
17
+ }
18
+
19
+ // Semantic UI checkbox focus styles
20
+ .ui.checkbox input:focus-visible ~ label::before {
21
+ .universal-focus-style();
22
+ outline-offset: 4px;
23
+ }
24
+
25
+ .ui.checkbox input:focus-visible ~ label::before {
26
+ transition: none !important;
27
+ }
28
+
29
+ // Skiplinks focus styles
30
+ .skiplinks-wrapper a:focus-visible {
31
+ border: 2px solid @linkColor;
32
+ outline: 3px solid @linkColor;
33
+ }