@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.
- package/CHANGELOG.md +22 -0
- package/locales/af/LC_MESSAGES/volto.po +37 -6
- package/locales/af.json +1 -1
- package/locales/ar/LC_MESSAGES/volto.po +37 -6
- package/locales/ar.json +1 -1
- package/locales/bg/LC_MESSAGES/volto.po +37 -6
- package/locales/bg.json +1 -1
- package/locales/bn/LC_MESSAGES/volto.po +37 -6
- package/locales/bn.json +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +37 -6
- package/locales/ca.json +1 -1
- package/locales/cs/LC_MESSAGES/volto.po +37 -6
- package/locales/cs.json +1 -1
- package/locales/cy/LC_MESSAGES/volto.po +37 -6
- package/locales/cy.json +1 -1
- package/locales/da/LC_MESSAGES/volto.po +37 -6
- package/locales/da.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +37 -6
- package/locales/de.json +1 -1
- package/locales/el/LC_MESSAGES/volto.po +37 -6
- package/locales/el.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +37 -6
- package/locales/en.json +1 -1
- package/locales/en_AU/LC_MESSAGES/volto.po +37 -6
- package/locales/en_AU.json +1 -1
- package/locales/en_GB/LC_MESSAGES/volto.po +37 -6
- package/locales/en_GB.json +1 -1
- package/locales/eo/LC_MESSAGES/volto.po +37 -6
- package/locales/eo.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +37 -6
- package/locales/es.json +1 -1
- package/locales/et/LC_MESSAGES/volto.po +37 -6
- package/locales/et.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +37 -6
- package/locales/eu.json +1 -1
- package/locales/fa/LC_MESSAGES/volto.po +37 -6
- package/locales/fa.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +37 -6
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +37 -6
- package/locales/fr.json +1 -1
- package/locales/fu/LC_MESSAGES/volto.po +37 -6
- package/locales/fu.json +1 -1
- package/locales/gl/LC_MESSAGES/volto.po +37 -6
- package/locales/gl.json +1 -1
- package/locales/he/LC_MESSAGES/volto.po +37 -6
- package/locales/he.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +37 -6
- package/locales/hi.json +1 -1
- package/locales/hr/LC_MESSAGES/volto.po +37 -6
- package/locales/hr.json +1 -1
- package/locales/hu/LC_MESSAGES/volto.po +37 -6
- package/locales/hu.json +1 -1
- package/locales/hy/LC_MESSAGES/volto.po +37 -6
- package/locales/hy.json +1 -1
- package/locales/id/LC_MESSAGES/volto.po +37 -6
- package/locales/id.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +37 -6
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +37 -6
- package/locales/ja.json +1 -1
- package/locales/ka/LC_MESSAGES/volto.po +37 -6
- package/locales/ka.json +1 -1
- package/locales/kn/LC_MESSAGES/volto.po +37 -6
- package/locales/kn.json +1 -1
- package/locales/ko/LC_MESSAGES/volto.po +37 -6
- package/locales/ko.json +1 -1
- package/locales/lt/LC_MESSAGES/volto.po +37 -6
- package/locales/lt.json +1 -1
- package/locales/lv/LC_MESSAGES/volto.po +37 -6
- package/locales/lv.json +1 -1
- package/locales/mi/LC_MESSAGES/volto.po +37 -6
- package/locales/mi.json +1 -1
- package/locales/mk/LC_MESSAGES/volto.po +37 -6
- package/locales/mk.json +1 -1
- package/locales/my/LC_MESSAGES/volto.po +37 -6
- package/locales/my.json +1 -1
- package/locales/nb_NO/LC_MESSAGES/volto.po +37 -6
- package/locales/nb_NO.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +37 -6
- package/locales/nl.json +1 -1
- package/locales/nn/LC_MESSAGES/volto.po +37 -6
- package/locales/nn.json +1 -1
- package/locales/pl/LC_MESSAGES/volto.po +37 -6
- package/locales/pl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +37 -6
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +37 -6
- package/locales/pt_BR.json +1 -1
- package/locales/rm/LC_MESSAGES/volto.po +37 -6
- package/locales/rm.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +37 -6
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +37 -6
- package/locales/ru.json +1 -1
- package/locales/sk/LC_MESSAGES/volto.po +37 -6
- package/locales/sk.json +1 -1
- package/locales/sl/LC_MESSAGES/volto.po +37 -6
- package/locales/sl.json +1 -1
- package/locales/sm/LC_MESSAGES/volto.po +37 -6
- package/locales/sm.json +1 -1
- package/locales/sq/LC_MESSAGES/volto.po +37 -6
- package/locales/sq.json +1 -1
- package/locales/sr/LC_MESSAGES/volto.po +37 -6
- package/locales/sr.json +1 -1
- package/locales/sr@cyrl/LC_MESSAGES/volto.po +37 -6
- package/locales/sr@cyrl.json +1 -1
- package/locales/sr@latn/LC_MESSAGES/volto.po +37 -6
- package/locales/sr@latn.json +1 -1
- package/locales/sv/LC_MESSAGES/volto.po +37 -6
- package/locales/sv.json +1 -1
- package/locales/ta/LC_MESSAGES/volto.po +37 -6
- package/locales/ta.json +1 -1
- package/locales/te/LC_MESSAGES/volto.po +37 -6
- package/locales/te.json +1 -1
- package/locales/th/LC_MESSAGES/volto.po +37 -6
- package/locales/th.json +1 -1
- package/locales/to/LC_MESSAGES/volto.po +37 -6
- package/locales/to.json +1 -1
- package/locales/tr/LC_MESSAGES/volto.po +37 -6
- package/locales/tr.json +1 -1
- package/locales/uk/LC_MESSAGES/volto.po +37 -6
- package/locales/uk.json +1 -1
- package/locales/vi/LC_MESSAGES/volto.po +37 -6
- package/locales/vi.json +1 -1
- package/locales/volto.pot +35 -4
- package/locales/zh_CN/LC_MESSAGES/volto.po +37 -6
- package/locales/zh_CN.json +1 -1
- package/locales/zh_Hant/LC_MESSAGES/volto.po +37 -6
- package/locales/zh_Hant.json +1 -1
- package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +37 -6
- package/locales/zh_Hant_HK.json +1 -1
- package/package.json +3 -3
- package/src/components/manage/Contents/Contents.jsx +3 -3
- package/src/components/manage/Contents/ContentsItem.jsx +1 -3
- package/src/components/manage/Contents/DropZoneContent.jsx +1 -0
- package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +23 -2
- package/src/components/manage/Toolbar/More.jsx +13 -0
- package/src/components/manage/Toolbar/Toolbar.jsx +91 -8
- package/src/components/manage/Widgets/FileWidget.jsx +71 -17
- package/src/components/manage/Widgets/RegistryImageWidget.jsx +4 -2
- package/src/components/theme/Sitemap/Sitemap.jsx +21 -28
- package/src/components/theme/Sitemap/Sitemap.test.jsx +18 -0
- package/src/config/index.js +1 -0
- 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
|
@@ -724,9 +724,9 @@ class Contents extends Component {
|
|
|
724
724
|
delta,
|
|
725
725
|
);
|
|
726
726
|
} else {
|
|
727
|
-
this.setState({
|
|
728
|
-
items: move(
|
|
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 =
|
|
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) => ({
|
|
@@ -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
|
-
|
|
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(
|
|
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)}
|
|
@@ -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
|
};
|
|
@@ -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,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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ({
|
|
103
|
+
promise: ({ store: { dispatch, getState } }) => {
|
|
110
104
|
if (!__SERVER__) return;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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}>
|
package/src/config/index.js
CHANGED
|
@@ -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
|
+
}
|