@plone/volto 19.1.1 → 19.1.3

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 (142) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/locales/af/LC_MESSAGES/volto.po +23 -9
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +23 -9
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +23 -9
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +23 -9
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +23 -9
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +23 -9
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +23 -9
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +23 -9
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +23 -9
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +23 -9
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +23 -9
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +23 -9
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +23 -9
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +23 -9
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +23 -9
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +23 -9
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +23 -9
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +23 -9
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +23 -9
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +23 -9
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +23 -9
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +23 -9
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +23 -9
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +23 -9
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +23 -9
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +23 -9
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +23 -9
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +23 -9
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +23 -9
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +23 -9
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +23 -9
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +23 -9
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +23 -9
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +23 -9
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +23 -9
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +23 -9
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +23 -9
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +23 -9
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +23 -9
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +23 -9
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +23 -9
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +23 -9
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +23 -9
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +23 -9
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +23 -9
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +23 -9
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +23 -9
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +23 -9
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +23 -9
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +23 -9
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +23 -9
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +23 -9
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +23 -9
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +23 -9
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +23 -9
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +23 -9
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +23 -9
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +23 -9
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +23 -9
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +23 -9
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +23 -9
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +23 -9
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +23 -9
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +23 -9
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +23 -9
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +23 -9
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +7 -7
  134. package/src/components/manage/Widgets/FileWidget.jsx +64 -20
  135. package/src/components/manage/Widgets/FormFieldWrapper.jsx +16 -4
  136. package/src/components/manage/Widgets/RegistryImageWidget.jsx +2 -2
  137. package/src/components/manage/Widgets/TextWidget.test.jsx +44 -0
  138. package/src/components/theme/Breadcrumbs/Breadcrumbs.jsx +1 -1
  139. package/src/components/theme/Login/Login.jsx +3 -0
  140. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +5 -1
  141. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.test.jsx +39 -9
  142. package/theme/themes/pastanaga/extras/main.less +1 -0
@@ -35,12 +35,12 @@ const messages = defineMessages({
35
35
  defaultMessage: 'Drop files here ...',
36
36
  },
37
37
  editFile: {
38
- id: 'Drop file here to replace the existing file',
39
- defaultMessage: 'Drop file here to replace the existing file',
38
+ id: 'Drop a file here or click to replace the existing file',
39
+ defaultMessage: 'Drop a file here or click to replace the existing file',
40
40
  },
41
41
  fileDrag: {
42
- id: 'Drop file here to upload a new file',
43
- defaultMessage: 'Drop file here to upload a new file',
42
+ id: 'Drop a file here or click to upload',
43
+ defaultMessage: 'Drop a file here or click to upload',
44
44
  },
45
45
  replaceFile: {
46
46
  id: 'Replace existing file',
@@ -60,8 +60,16 @@ const messages = defineMessages({
60
60
  defaultMessage: 'File is not of the accepted type {accept}',
61
61
  },
62
62
  dragAndDropActionA11y: {
63
- id: 'Press Enter to browse files from your computer.',
64
- defaultMessage: 'Press Enter to browse files from your computer.',
63
+ id: 'File upload area. Press Enter to open the file browser',
64
+ defaultMessage: 'File upload area. Press Enter to open the file browser',
65
+ },
66
+ requiredField: {
67
+ id: 'This field is required.',
68
+ defaultMessage: 'This field is required.',
69
+ },
70
+ downloadFile: {
71
+ id: 'Download {filename}',
72
+ defaultMessage: 'Download {filename}',
65
73
  },
66
74
  });
67
75
 
@@ -89,7 +97,7 @@ const messages = defineMessages({
89
97
  *
90
98
  */
91
99
  const FileWidget = (props) => {
92
- const { id, value, onChange, isDisabled } = props;
100
+ const { id, value, onChange, isDisabled, fieldSet } = props;
93
101
  const [fileType, setFileType] = React.useState(false);
94
102
  const intl = useIntl();
95
103
 
@@ -171,6 +179,16 @@ const FileWidget = (props) => {
171
179
  reader.readAsDataURL(files[0]);
172
180
  };
173
181
 
182
+ const statusTextA11y = [
183
+ intl.formatMessage(messages.dragAndDropActionA11y), // Interaction instructions
184
+ props.required && intl.formatMessage(messages.requiredField), // Required field status
185
+ value?.filename, // Current file name if a file is uploaded
186
+ ]
187
+ .filter(Boolean)
188
+ .join('. ');
189
+
190
+ const errorTextA11y = props.error?.length ? props.error.join('. ') : null;
191
+
174
192
  return (
175
193
  <FormFieldWrapper {...props}>
176
194
  <Dropzone
@@ -179,7 +197,13 @@ const FileWidget = (props) => {
179
197
  {...(props.accept ? { accept: props.accept } : {})}
180
198
  >
181
199
  {({ getRootProps, getInputProps, isDragActive }) => (
182
- <div className="file-widget-dropzone" {...getRootProps()}>
200
+ <div
201
+ className="file-widget-dropzone"
202
+ role="button"
203
+ aria-labelledby={`fieldset-${fieldSet}-field-label-${id}`}
204
+ aria-describedby={`field-${id}-status`}
205
+ {...getRootProps()}
206
+ >
183
207
  {isDragActive && <Dimmer active></Dimmer>}
184
208
  {fileType ? (
185
209
  <Image
@@ -205,34 +229,52 @@ const FileWidget = (props) => {
205
229
  </div>
206
230
  )}
207
231
 
208
- <label className="label-file-widget-input">
232
+ {/* aria-hidden: keyboard access is handled by the parent div (role="button").
233
+ The label is a visual affordance only. The stopPropagation prevents the Dropzone
234
+ from opening the file dialog twice on click. */}
235
+ <label
236
+ className="label-file-widget-input"
237
+ htmlFor={`field-${id}`}
238
+ aria-hidden="true"
239
+ onClick={(e) => e.stopPropagation()}
240
+ >
209
241
  {value
210
242
  ? intl.formatMessage(messages.replaceFile)
211
243
  : intl.formatMessage(messages.addNewFile)}
212
- <span className="visually-hidden">
213
- {intl.formatMessage(messages.dragAndDropActionA11y)}
214
- </span>
215
244
  </label>
245
+ <span id={`field-${id}-status`} className="visually-hidden">
246
+ {statusTextA11y}
247
+ </span>
248
+ {errorTextA11y && (
249
+ <span role="alert" className="visually-hidden">
250
+ {errorTextA11y}
251
+ </span>
252
+ )}
216
253
  <input
217
254
  {...getInputProps({
218
255
  type: 'file',
219
256
  style: { display: 'none' },
220
257
  })}
221
258
  id={`field-${id}`}
259
+ {...(props.required && { 'aria-required': true })}
260
+ {...(props.error?.length > 0 && { 'aria-invalid': true })}
222
261
  name={id}
223
- type="file"
224
262
  disabled={isDisabled}
225
263
  />
226
264
  </div>
227
265
  )}
228
266
  </Dropzone>
229
- <div className="field-file-name">
230
- {value && (
231
- <UniversalLink href={value.download} download={true}>
267
+ {value && (
268
+ <div className="field-file-name">
269
+ <UniversalLink
270
+ href={value.download}
271
+ aria-label={intl.formatMessage(messages.downloadFile, {
272
+ filename: value.filename,
273
+ })}
274
+ download={true}
275
+ >
232
276
  {value.filename}
233
277
  </UniversalLink>
234
- )}
235
- {value && (
236
278
  <Button
237
279
  type="button"
238
280
  icon
@@ -247,8 +289,8 @@ const FileWidget = (props) => {
247
289
  >
248
290
  <Icon name={deleteSVG} size="20px" />
249
291
  </Button>
250
- )}
251
- </div>
292
+ </div>
293
+ )}
252
294
  </FormFieldWrapper>
253
295
  );
254
296
  };
@@ -263,6 +305,7 @@ FileWidget.propTypes = {
263
305
  title: PropTypes.string.isRequired,
264
306
  description: PropTypes.string,
265
307
  required: PropTypes.bool,
308
+ fieldSet: PropTypes.string,
266
309
  error: PropTypes.arrayOf(PropTypes.string),
267
310
  value: PropTypes.shape({
268
311
  '@type': PropTypes.string,
@@ -280,6 +323,7 @@ FileWidget.propTypes = {
280
323
  FileWidget.defaultProps = {
281
324
  description: null,
282
325
  required: false,
326
+ fieldSet: null,
283
327
  error: [],
284
328
  value: null,
285
329
  };
@@ -2,9 +2,9 @@
2
2
  * FormFieldWrapper component.
3
3
  * @module components/manage/Widgets/FormFieldWrapper
4
4
  */
5
- import React from 'react';
5
+ import React, { Children, isValidElement, cloneElement } from 'react';
6
6
  import PropTypes from 'prop-types';
7
- import { Form, Grid, Icon as IconOld, Label } from 'semantic-ui-react';
7
+ import { Form, Grid, Icon as IconOld, Input, Label } from 'semantic-ui-react';
8
8
  import map from 'lodash/map';
9
9
  import cx from 'classnames';
10
10
  import { defineMessages, useIntl } from 'react-intl';
@@ -59,9 +59,21 @@ const FormFieldWrapper = ({
59
59
 
60
60
  const wdg = (
61
61
  <>
62
- {children}
62
+ {Children.map(children, (child) => {
63
+ if (isValidElement(child) && required && child.type === Input) {
64
+ return cloneElement(child, {
65
+ 'aria-required': true,
66
+ 'aria-invalid': !!(error && error.length > 0),
67
+ });
68
+ }
69
+ return child;
70
+ })}
63
71
 
64
- <div aria-live="polite" aria-atomic="true">
72
+ <div
73
+ aria-live="polite"
74
+ aria-atomic="true"
75
+ className={cx({ 'visually-hidden': !error?.length })}
76
+ >
65
77
  {map(error, (message) => (
66
78
  <Label key={message} basic color="red" className="form-error-label">
67
79
  {message}
@@ -33,8 +33,8 @@ const messages = defineMessages({
33
33
  defaultMessage: 'Drop files here ...',
34
34
  },
35
35
  editFile: {
36
- id: 'Drop file here to replace the existing file',
37
- defaultMessage: 'Drop file here to replace the existing file',
36
+ id: 'Drop a file here or click to replace the existing file',
37
+ defaultMessage: 'Drop a file here or click to replace the existing file',
38
38
  },
39
39
  fileDrag: {
40
40
  id: 'Drop file here to upload a new file',
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
+ import { render, screen } from '@testing-library/react';
3
4
  import configureStore from 'redux-mock-store';
4
5
  import { Provider } from 'react-intl-redux';
5
6
 
@@ -30,3 +31,46 @@ test('renders a text widget component', () => {
30
31
  const json = component.toJSON();
31
32
  expect(json).toMatchSnapshot();
32
33
  });
34
+
35
+ test('adds aria-required attribute to input when required prop is true', () => {
36
+ const store = mockStore({
37
+ intl: { locale: 'en', messages: {} },
38
+ });
39
+
40
+ render(
41
+ <Provider store={store}>
42
+ <TextWidget
43
+ id="my-field"
44
+ title="My field"
45
+ onChange={() => {}}
46
+ onBlur={() => {}}
47
+ onClick={() => {}}
48
+ required={true}
49
+ />
50
+ </Provider>,
51
+ );
52
+
53
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-required', 'true');
54
+ });
55
+
56
+ test('adds aria-invalid attribute to input when field has errors', () => {
57
+ const store = mockStore({
58
+ intl: { locale: 'en', messages: {} },
59
+ });
60
+
61
+ render(
62
+ <Provider store={store}>
63
+ <TextWidget
64
+ id="my-field"
65
+ title="My field"
66
+ onChange={() => {}}
67
+ onBlur={() => {}}
68
+ onClick={() => {}}
69
+ required={true}
70
+ error={['This field is required']}
71
+ />
72
+ </Provider>,
73
+ );
74
+
75
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
76
+ });
@@ -67,7 +67,7 @@ const BreadcrumbsComponent = ({ pathname }) => {
67
67
  className="section"
68
68
  title={intl.formatMessage(messages.home)}
69
69
  >
70
- <Icon name={homeSVG} size="18px" />
70
+ <Icon name={homeSVG} size="24px" />
71
71
  </Link>
72
72
  {items.map((item, index, items) => [
73
73
  <Breadcrumb.Divider key={`divider-${item.url}`} />,
@@ -14,6 +14,7 @@ import qs from 'query-string';
14
14
 
15
15
  import Helmet from '@plone/volto/helpers/Helmet/Helmet';
16
16
  import { usePrevious } from '@plone/volto/helpers/Utils/usePrevious';
17
+ import { useClient } from '@plone/volto/hooks/client/useClient';
17
18
  import config from '@plone/volto/registry';
18
19
  import Icon from '@plone/volto/components/theme/Icon/Icon';
19
20
  import {
@@ -84,6 +85,7 @@ const Login = (props) => {
84
85
  location.pathname.replace(/\/[^/]*\/?$/, '') ||
85
86
  '/';
86
87
  const previousToken = usePrevious(token);
88
+ const isClient = useClient();
87
89
 
88
90
  useEffect(() => {
89
91
  if (location?.state?.isLogout) {
@@ -252,6 +254,7 @@ const Login = (props) => {
252
254
  aria-label={intl.formatMessage(messages.login)}
253
255
  title={intl.formatMessage(messages.login)}
254
256
  loading={loading}
257
+ disabled={!isClient}
255
258
  >
256
259
  <Icon className="circled" name={aheadSVG} size="30px" />
257
260
  </Button>
@@ -6,7 +6,7 @@ import { changeLanguage } from '@plone/volto/actions/language/language';
6
6
  import { toGettextLang, toBackendLang } from '@plone/volto/helpers/Utils/Utils';
7
7
 
8
8
  const MultilingualRedirector = (props) => {
9
- const { pathname, children } = props;
9
+ const { pathname, contentLanguage, children } = props;
10
10
  const [cookies] = useCookies();
11
11
  const site = useSelector((state) => state.site.data);
12
12
  const isMultilingual = site.features?.multilingual;
@@ -44,8 +44,11 @@ const MultilingualRedirector = (props) => {
44
44
  performLanguageSwitch(redirectToLanguage);
45
45
  } else {
46
46
  const lang = pathname.split('/')[1];
47
+ const contentLanguageMatchesPath =
48
+ !contentLanguage || contentLanguage === lang;
47
49
  if (
48
50
  availableLanguages?.includes(lang) &&
51
+ contentLanguageMatchesPath &&
49
52
  lang !== toBackendLang(currentLanguage)
50
53
  ) {
51
54
  performLanguageSwitch(lang);
@@ -62,6 +65,7 @@ const MultilingualRedirector = (props) => {
62
65
  isMultilingual,
63
66
  availableLanguages,
64
67
  currentLanguage,
68
+ contentLanguage,
65
69
  ]);
66
70
  return pathname === '/' && isMultilingual ? (
67
71
  <Redirect to={`/${toBackendLang(redirectToLanguage)}`} />
@@ -10,6 +10,17 @@ import MultilingualRedirector from './MultilingualRedirector';
10
10
  const mockStore = configureStore();
11
11
 
12
12
  describe('MultilingualRedirector', () => {
13
+ const renderComponent = (store, props) =>
14
+ renderer.create(
15
+ <Provider store={store}>
16
+ <CookiesProvider>
17
+ <MemoryRouter>
18
+ <MultilingualRedirector {...props} />
19
+ </MemoryRouter>
20
+ </CookiesProvider>
21
+ </Provider>,
22
+ );
23
+
13
24
  it('renders a MultilingualRedirector component', () => {
14
25
  const store = mockStore({
15
26
  site: {
@@ -20,16 +31,35 @@ describe('MultilingualRedirector', () => {
20
31
  messages: {},
21
32
  },
22
33
  });
23
- const component = renderer.create(
24
- <Provider store={store}>
25
- <CookiesProvider>
26
- <MemoryRouter>
27
- <MultilingualRedirector pathname={'/'} />
28
- </MemoryRouter>
29
- </CookiesProvider>
30
- </Provider>,
31
- );
34
+ const component = renderComponent(store, { pathname: '/' });
32
35
  const json = component.toJSON();
33
36
  expect(json).toMatchSnapshot();
34
37
  });
38
+
39
+ it('does not switch to the path language when content language differs', () => {
40
+ const store = mockStore({
41
+ site: {
42
+ data: {
43
+ features: {
44
+ multilingual: true,
45
+ },
46
+ 'plone.available_languages': ['en', 'es'],
47
+ 'plone.default_language': 'en',
48
+ },
49
+ },
50
+ intl: {
51
+ locale: 'es',
52
+ messages: {},
53
+ },
54
+ });
55
+
56
+ renderer.act(() => {
57
+ renderComponent(store, {
58
+ pathname: '/en/document',
59
+ contentLanguage: 'es',
60
+ });
61
+ });
62
+
63
+ expect(store.getActions()).toEqual([]);
64
+ });
35
65
  });
@@ -485,6 +485,7 @@ fieldset.invisible {
485
485
  }
486
486
 
487
487
  .file-widget-dropzone {
488
+ padding-bottom: 1rem;
488
489
  outline: none;
489
490
  }
490
491