@plone/volto 18.9.1 → 18.9.2

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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +0 -1
  3. package/package.json +3 -3
  4. package/src/components/manage/Add/Add.jsx +5 -1
  5. package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +2 -0
  6. package/src/components/manage/BlockChooser/BlockChooser.jsx +1 -0
  7. package/src/components/manage/BlockChooser/BlockChooserButton.jsx +1 -0
  8. package/src/components/manage/BlockChooser/BlockChooserSearch.jsx +1 -0
  9. package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +1 -0
  10. package/src/components/manage/Blocks/Container/EditBlockWrapper.jsx +2 -0
  11. package/src/components/manage/Blocks/Container/NewBlockAddButton.jsx +1 -0
  12. package/src/components/manage/Blocks/Container/SimpleContainerToolbar.jsx +2 -0
  13. package/src/components/manage/Blocks/HTML/Edit.jsx +9 -1
  14. package/src/components/manage/Blocks/Image/ImageSidebar.jsx +1 -0
  15. package/src/components/manage/Blocks/Listing/ImageGallery.jsx +2 -0
  16. package/src/components/manage/Blocks/Maps/Edit.jsx +2 -0
  17. package/src/components/manage/Blocks/Search/components/Facets.jsx +1 -0
  18. package/src/components/manage/Blocks/Search/components/FilterList.jsx +1 -0
  19. package/src/components/manage/Blocks/Search/components/SearchInput.jsx +1 -0
  20. package/src/components/manage/Blocks/Search/components/SortOn.jsx +2 -0
  21. package/src/components/manage/Blocks/Teaser/Data.jsx +2 -0
  22. package/src/components/manage/Blocks/Video/Edit.jsx +2 -0
  23. package/src/components/manage/Delete/Delete.jsx +1 -0
  24. package/src/components/manage/Diff/Diff.jsx +1 -0
  25. package/src/components/manage/Edit/Edit.jsx +1 -0
  26. package/src/components/manage/Form/Form.jsx +1 -0
  27. package/src/components/manage/Form/ModalForm.jsx +1 -0
  28. package/src/components/manage/Form/UndoToolbar.jsx +2 -0
  29. package/src/components/manage/Sidebar/AlignBlock.jsx +1 -0
  30. package/src/components/manage/Sidebar/ObjectBrowserNav.jsx +1 -0
  31. package/src/components/manage/Sidebar/Sidebar.jsx +2 -0
  32. package/src/components/manage/TemplateChooser/TemplateChooser.jsx +1 -0
  33. package/src/components/manage/Widgets/CheckboxWidget.jsx +1 -0
  34. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +2 -1
  35. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +67 -10
  36. package/src/components/theme/Image/Image.jsx +1 -2
  37. package/src/components/theme/Image/Image.test.jsx +8 -0
  38. package/src/storybook.jsx +2 -0
package/CHANGELOG.md CHANGED
@@ -17,6 +17,20 @@ myst:
17
17
 
18
18
  <!-- towncrier release notes start -->
19
19
 
20
+ ## 18.9.2 (2025-03-07)
21
+
22
+ ### Bugfix
23
+
24
+ - Does not show empty `class` attribute when rendering `img` tag with `Image` component. @wesleybl [#6788](https://github.com/plone/volto/issues/6788)
25
+ - Fixed button types across several components to allow reuse in more complex forms @pnicolli [#6791](https://github.com/plone/volto/issues/6791)
26
+ - Fixed `AlternateHrefLangs` component, missing `FlattenToAppURL`. @sneridagh [#6799](https://github.com/plone/volto/issues/6799)
27
+ - a11y - Added id attribute to checkbox widget for proper identification and fixes label functionality for screen readers. @Wagner3UB [#6802](https://github.com/plone/volto/issues/6802)
28
+
29
+ ### Documentation
30
+
31
+ - Improve the wording on theming docs. @erral [#6767](https://github.com/plone/volto/issues/6767)
32
+ - Fix MyST warnings about bad references, remove cookiecutter-plone-starter link, and remove `rietveldschroderhuis.nl` due to repeated failures and no response in https://github.com/collective/awesome-volto/pull/27. @stevepiercy [#6812](https://github.com/plone/volto/issues/6812)
33
+
20
34
  ## 18.9.1 (2025-02-20)
21
35
 
22
36
  ### Bugfix
package/README.md CHANGED
@@ -194,5 +194,4 @@ You should check the dependencies in their `package.json` for more details.
194
194
  - [volto-centraalmuseum-theme](https://github.com/intk/volto-centraalmuseum-theme) - Volto project for the [Centraal Museum & Rietveld](https://www.centraalmuseum.nl/nl) made for [INTK](https://www.intk.com/en).
195
195
  - [volto-eea-design-system](https://github.com/eea/volto-eea-design-system) - EEA Design System Plone 6 Kit Volto project for [European Environment Agency web sites](https://eea.github.io/volto-eea-design-system/)
196
196
  - [volto-eea-kitkat](https://github.com/eea/volto-eea-kitkat) - A known good set of Volto add-ons to be used within all EEA projects and beyond, made for [European Environment Agency](https://www.eea.europa.eu/en)
197
- - [volto-rietveldschroderhuis-theme](https://github.com/intk/volto-rietveldschroderhuis-theme) - Volto project for the [Rietveld Schröder House](https://www.rietveldschroderhuis.nl/en) made for [INTK](https://www.intk.com/en).
198
197
  - [volto-zeeuwsmuseum-theme](https://github.com/intk/volto-zeeuwsmuseum-theme) - Volto project for the [Zeeuws Museum](https://www.zeeuwsmuseum.nl/en) made for [INTK](https://www.intk.com/en).
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "18.9.1",
12
+ "version": "18.9.2",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -237,8 +237,8 @@
237
237
  "use-deep-compare-effect": "1.8.1",
238
238
  "uuid": "^8.3.2",
239
239
  "@plone/registry": "2.4.1",
240
- "@plone/scripts": "3.8.2",
241
- "@plone/volto-slate": "18.2.2"
240
+ "@plone/volto-slate": "18.2.3",
241
+ "@plone/scripts": "3.8.2"
242
242
  },
243
243
  "devDependencies": {
244
244
  "@babel/core": "^7.0.0",
@@ -425,7 +425,11 @@ class Add extends Component {
425
425
  title={this.props.intl.formatMessage(messages.save)}
426
426
  />
427
427
  </Button>
428
- <Button className="cancel" onClick={() => this.onCancel()}>
428
+ <Button
429
+ className="cancel"
430
+ onClick={() => this.onCancel()}
431
+ type="button"
432
+ >
429
433
  <Icon
430
434
  name={clearSVG}
431
435
  className="circled"
@@ -258,6 +258,7 @@ class AddLinkForm extends Component {
258
258
  {value.length > 0 ? (
259
259
  <Button.Group>
260
260
  <Button
261
+ type="button"
261
262
  basic
262
263
  className="cancel"
263
264
  aria-label={this.props.intl.formatMessage(messages.clear)}
@@ -274,6 +275,7 @@ class AddLinkForm extends Component {
274
275
  ) : this.props.objectBrowserPickerType === 'link' ? (
275
276
  <Button.Group>
276
277
  <Button
278
+ type="button"
277
279
  basic
278
280
  icon
279
281
  aria-label={this.props.intl.formatMessage(
@@ -128,6 +128,7 @@ const BlockChooser = ({
128
128
  return (
129
129
  <Button.Group key={block.id}>
130
130
  <Button
131
+ type="button"
131
132
  icon
132
133
  basic
133
134
  className={block.id}
@@ -29,6 +29,7 @@ export const ButtonComponent = (props) => {
29
29
 
30
30
  return (
31
31
  <Button
32
+ type="button"
32
33
  icon
33
34
  basic
34
35
  title={intl.formatMessage(messages.addBlock)}
@@ -38,6 +38,7 @@ const BlockChooserSearch = ({ onChange, searchValue }) => {
38
38
  />
39
39
  {searchValue && (
40
40
  <Button
41
+ type="button"
41
42
  className="clear-search-button"
42
43
  aria-label={intl.formatMessage(messages.clear)}
43
44
  onClick={() => {
@@ -106,6 +106,7 @@ const EditBlockWrapper = (props) => {
106
106
  {children}
107
107
  {selected && !required && editable && (
108
108
  <Button
109
+ type="button"
109
110
  icon
110
111
  basic
111
112
  onClick={() => onDeleteBlock(block, true)}
@@ -66,6 +66,7 @@ const EditBlockWrapper = (props) => {
66
66
  aria-label={intl.formatMessage(messages.reset, {
67
67
  index,
68
68
  })}
69
+ type="button"
69
70
  basic
70
71
  icon
71
72
  onClick={(e) => onResetBlock(block, {})}
@@ -75,6 +76,7 @@ const EditBlockWrapper = (props) => {
75
76
  </Button>
76
77
  ) : (
77
78
  <Button
79
+ type="button"
78
80
  basic
79
81
  icon
80
82
  className="remove-block-button"
@@ -49,6 +49,7 @@ const NewBlockAddButton = (props) => {
49
49
  <>
50
50
  <Ref innerRef={setReferenceElement}>
51
51
  <Button
52
+ type="button"
52
53
  basic
53
54
  icon
54
55
  onClick={() => setOpenMenu(true)}
@@ -25,6 +25,7 @@ const SimpleContainerToolbar = (props) => {
25
25
  <Button.Group>
26
26
  <Button
27
27
  aria-label={intl.formatMessage(messages.addBlock)}
28
+ type="button"
28
29
  icon
29
30
  basic
30
31
  disabled={data?.blocks_layout?.items?.length >= maxLength}
@@ -36,6 +37,7 @@ const SimpleContainerToolbar = (props) => {
36
37
  <Button.Group>
37
38
  <Button
38
39
  aria-label={intl.formatMessage(messages.blockSettings)}
40
+ type="button"
39
41
  icon
40
42
  basic
41
43
  onClick={(e) => {
@@ -248,6 +248,7 @@ class Edit extends Component {
248
248
  <Popup
249
249
  trigger={
250
250
  <Button
251
+ type="button"
251
252
  icon
252
253
  basic
253
254
  aria-label={this.props.intl.formatMessage(messages.source)}
@@ -264,6 +265,7 @@ class Edit extends Component {
264
265
  <Popup
265
266
  trigger={
266
267
  <Button
268
+ type="button"
267
269
  icon
268
270
  basic
269
271
  aria-label={this.props.intl.formatMessage(messages.preview)}
@@ -280,6 +282,7 @@ class Edit extends Component {
280
282
  <Popup
281
283
  trigger={
282
284
  <Button
285
+ type="button"
283
286
  icon
284
287
  basic
285
288
  aria-label={this.props.intl.formatMessage(messages.prettier)}
@@ -296,7 +299,12 @@ class Edit extends Component {
296
299
  <Popup
297
300
  trigger={
298
301
  <Button.Group>
299
- <Button icon basic onClick={() => this.onChangeCode('')}>
302
+ <Button
303
+ type="button"
304
+ icon
305
+ basic
306
+ onClick={() => this.onChangeCode('')}
307
+ >
300
308
  <Icon name={clearSVG} size="24px" color="#e40166" />
301
309
  </Button>
302
310
  </Button.Group>
@@ -31,6 +31,7 @@ const ImageSidebar = (props) => {
31
31
  <Button.Group>
32
32
  <Button
33
33
  title={intl.formatMessage(messages.clear)}
34
+ type="button"
34
35
  basic
35
36
  disabled={!data.url}
36
37
  onClick={() => {
@@ -19,6 +19,7 @@ const ImageGallery = loadable(() => import('react-image-gallery'));
19
19
  const renderLeftNav = (onClick, disabled) => {
20
20
  return (
21
21
  <Button
22
+ type="button"
22
23
  className="image-gallery-icon image-gallery-left-nav primary basic"
23
24
  disabled={disabled}
24
25
  onClick={onClick}
@@ -30,6 +31,7 @@ const renderLeftNav = (onClick, disabled) => {
30
31
  const renderRightNav = (onClick, disabled) => {
31
32
  return (
32
33
  <Button
34
+ type="button"
33
35
  className="image-gallery-icon image-gallery-right-nav primary basic"
34
36
  disabled={disabled}
35
37
  onClick={onClick}
@@ -135,6 +135,7 @@ const Edit = React.memo((props) => {
135
135
  {url && (
136
136
  <Button.Group>
137
137
  <Button
138
+ type="button"
138
139
  basic
139
140
  className="cancel"
140
141
  onClick={(e) => {
@@ -148,6 +149,7 @@ const Edit = React.memo((props) => {
148
149
  )}
149
150
  <Button.Group>
150
151
  <Button
152
+ type="button"
151
153
  basic
152
154
  primary
153
155
  onClick={(e) => {
@@ -145,6 +145,7 @@ const Facets = (props) => {
145
145
  className="toggle-advanced-facets"
146
146
  >
147
147
  <Button
148
+ type="button"
148
149
  onClick={() => {
149
150
  setHidden((prevHidden) => !prevHidden);
150
151
  }}
@@ -53,6 +53,7 @@ const FilterList = (props) => {
53
53
  {intl.formatMessage(messages.currentFilters)}: {totalFilters}
54
54
  </div>
55
55
  <Button
56
+ type="button"
56
57
  icon
57
58
  basic
58
59
  compact
@@ -46,6 +46,7 @@ const SearchInput = (props) => {
46
46
  <div className="search-input-actions">
47
47
  {searchText && (
48
48
  <Button
49
+ type="button"
49
50
  basic
50
51
  icon
51
52
  className="search-input-clear-icon-button"
@@ -113,6 +113,7 @@ const SortOn = (props) => {
113
113
  {activeSortOn ? (
114
114
  <>
115
115
  <Button
116
+ type="button"
116
117
  icon
117
118
  basic
118
119
  compact
@@ -127,6 +128,7 @@ const SortOn = (props) => {
127
128
  <Icon name={downSVG} size="25px" />
128
129
  </Button>
129
130
  <Button
131
+ type="button"
130
132
  icon
131
133
  basic
132
134
  compact
@@ -117,6 +117,7 @@ const TeaserData = (props) => {
117
117
  <Button.Group>
118
118
  <Button
119
119
  aria-label={intl.formatMessage(messages.resetTeaser)}
120
+ type="button"
120
121
  basic
121
122
  disabled={isReseteable}
122
123
  onClick={() => reset()}
@@ -130,6 +131,7 @@ const TeaserData = (props) => {
130
131
  <Button.Group className="refresh teaser">
131
132
  <Button
132
133
  aria-label={intl.formatMessage(messages.refreshTeaser)}
134
+ type="button"
133
135
  basic
134
136
  onClick={() => refresh()}
135
137
  disabled={isEmpty(data.href)}
@@ -96,6 +96,7 @@ const Edit = (props) => {
96
96
  {url && (
97
97
  <Button.Group>
98
98
  <Button
99
+ type="button"
99
100
  basic
100
101
  className="cancel"
101
102
  onClick={(e) => {
@@ -109,6 +110,7 @@ const Edit = (props) => {
109
110
  )}
110
111
  <Button.Group>
111
112
  <Button
113
+ type="button"
112
114
  basic
113
115
  primary
114
116
  onClick={(e) => {
@@ -101,6 +101,7 @@ const Delete = () => {
101
101
  onClick={onSubmit}
102
102
  />
103
103
  <Button
104
+ type="button"
104
105
  basic
105
106
  circular
106
107
  secondary
@@ -249,6 +249,7 @@ class Diff extends Component {
249
249
  ],
250
250
  (view) => (
251
251
  <Button
252
+ type="button"
252
253
  key={view.id}
253
254
  value={view.id}
254
255
  active={this.props.view === view.id}
@@ -447,6 +447,7 @@ class Edit extends Component {
447
447
  />
448
448
  </Button>
449
449
  <Button
450
+ type="button"
450
451
  className="cancel"
451
452
  aria-label={this.props.intl.formatMessage(messages.cancel)}
452
453
  onClick={() => this.onCancel()}
@@ -1020,6 +1020,7 @@ class Form extends Component {
1020
1020
  )}
1021
1021
  {onCancel && (
1022
1022
  <Button
1023
+ type="button"
1023
1024
  basic
1024
1025
  secondary
1025
1026
  aria-label={this.props.intl.formatMessage(
@@ -308,6 +308,7 @@ class ModalForm extends Component {
308
308
  </Button>
309
309
  {onCancel && (
310
310
  <Button
311
+ type="button"
311
312
  basic
312
313
  circular
313
314
  secondary
@@ -39,6 +39,7 @@ const UndoToolbar = ({ state, onUndoRedo, maxUndoLevels, enableHotKeys }) => {
39
39
  dependencies={[canUndo, canRedo]}
40
40
  >
41
41
  <Button
42
+ type="button"
42
43
  className="undo"
43
44
  onClick={() => doUndo()}
44
45
  aria-label={intl.formatMessage(messages.undo)}
@@ -58,6 +59,7 @@ const UndoToolbar = ({ state, onUndoRedo, maxUndoLevels, enableHotKeys }) => {
58
59
  dependencies={[canUndo, canRedo]}
59
60
  >
60
61
  <Button
62
+ type="button"
61
63
  className="redo"
62
64
  onClick={() => doRedo()}
63
65
  aria-label={intl.formatMessage(messages.redo)}
@@ -59,6 +59,7 @@ const AlignBlock = ({
59
59
  {actions.map((action) => (
60
60
  <Button.Group key={action}>
61
61
  <Button
62
+ type="button"
62
63
  icon
63
64
  basic
64
65
  aria-label={intl.formatMessage(messages[action])}
@@ -164,6 +164,7 @@ const ObjectBrowserNav = ({
164
164
  >
165
165
  <Button.Group>
166
166
  <Button
167
+ type="button"
167
168
  basic
168
169
  icon
169
170
  aria-label={`${intl.formatMessage(messages.browse)} ${
@@ -113,6 +113,7 @@ const Sidebar = (props) => {
113
113
  style={size > 0 ? { width: size } : null}
114
114
  >
115
115
  <Button
116
+ type="button"
116
117
  aria-label={
117
118
  expanded
118
119
  ? intl.formatMessage(messages.shrinkSidebar)
@@ -126,6 +127,7 @@ const Sidebar = (props) => {
126
127
  onClick={onToggleExpanded}
127
128
  />
128
129
  <Button
130
+ type="button"
129
131
  className="full-size-sidenav-btn"
130
132
  onClick={onToggleFullSize}
131
133
  aria-label="full-screen-sidenav"
@@ -11,6 +11,7 @@ const TemplateChooser = ({ templates, onSelectTemplate }) => {
11
11
  {templates(intl).map((template, index) => (
12
12
  <Grid.Column key={template.id}>
13
13
  <Button
14
+ type="button"
14
15
  className="template-chooser-item"
15
16
  onClick={() => onSelectTemplate(index)}
16
17
  >
@@ -31,6 +31,7 @@ const CheckboxWidget = (props) => {
31
31
  <FormFieldWrapper {...props} columns={1}>
32
32
  <div className="wrapper">
33
33
  <Checkbox
34
+ id={`field-${id}`}
34
35
  name={`field-${id}`}
35
36
  checked={value || false}
36
37
  disabled={isDisabled}
@@ -1,5 +1,6 @@
1
1
  import config from '@plone/volto/registry';
2
2
  import Helmet from '@plone/volto/helpers/Helmet/Helmet';
3
+ import { flattenToAppURL, toPublicURL } from '@plone/volto/helpers/Url/Url';
3
4
 
4
5
  const AlternateHrefLangs = (props) => {
5
6
  const { content } = props;
@@ -16,7 +17,7 @@ const AlternateHrefLangs = (props) => {
16
17
  key={key}
17
18
  rel="alternate"
18
19
  hrefLang={item.language}
19
- href={item['@id']}
20
+ href={toPublicURL(flattenToAppURL(item['@id']))}
20
21
  />
21
22
  );
22
23
  })}
@@ -35,16 +35,18 @@ describe('AlternateHrefLangs', () => {
35
35
  const helmetLinks = Helmet.peek().linkTags;
36
36
  expect(helmetLinks.length).toBe(0);
37
37
  });
38
+
38
39
  it('multilingual site, with some translations', () => {
40
+ config.settings.publicURL = 'https://plone.org';
39
41
  config.settings.isMultilingual = true;
40
42
  config.settings.supportedLanguages = ['en', 'es', 'eu'];
41
43
 
42
44
  const content = {
43
- '@id': '/en',
45
+ '@id': 'http://localhost:8080/Plone/en',
44
46
  language: { token: 'en', title: 'English' },
45
47
  '@components': {
46
48
  translations: {
47
- items: [{ '@id': '/es', language: 'es' }],
49
+ items: [{ '@id': 'http://localhost:8080/Plone/es', language: 'es' }],
48
50
  },
49
51
  },
50
52
  };
@@ -71,17 +73,72 @@ describe('AlternateHrefLangs', () => {
71
73
 
72
74
  expect(helmetLinks).toContainEqual({
73
75
  rel: 'alternate',
74
- href: '/es',
76
+ href: 'https://plone.org/es',
75
77
  hrefLang: 'es',
76
78
  });
77
79
  expect(helmetLinks).toContainEqual({
78
80
  rel: 'alternate',
79
- href: '/en',
81
+ href: 'https://plone.org/en',
80
82
  hrefLang: 'en',
81
83
  });
82
84
  });
83
85
 
84
86
  it('multilingual site, with all available translations', () => {
87
+ config.settings.publicURL = 'https://plone.org';
88
+ config.settings.isMultilingual = true;
89
+ config.settings.supportedLanguages = ['en', 'es', 'eu'];
90
+ const store = mockStore({
91
+ intl: {
92
+ locale: 'en',
93
+ messages: {},
94
+ },
95
+ });
96
+
97
+ const content = {
98
+ '@id': 'http://localhost:8080/Plone/en',
99
+ language: { token: 'en', title: 'English' },
100
+ '@components': {
101
+ translations: {
102
+ items: [
103
+ { '@id': 'http://localhost:8080/Plone/eu', language: 'eu' },
104
+ { '@id': 'http://localhost:8080/Plone/es', language: 'es' },
105
+ ],
106
+ },
107
+ },
108
+ };
109
+
110
+ // We need to force the component rendering
111
+ // to fill the Helmet
112
+ renderer.create(
113
+ <Provider store={store}>
114
+ <AlternateHrefLangs content={content} />
115
+ </Provider>,
116
+ );
117
+
118
+ const helmetLinks = Helmet.peek().linkTags;
119
+
120
+ // We expect having 3 links
121
+ expect(helmetLinks.length).toBe(3);
122
+
123
+ expect(helmetLinks).toContainEqual({
124
+ rel: 'alternate',
125
+ href: 'https://plone.org/eu',
126
+ hrefLang: 'eu',
127
+ });
128
+ expect(helmetLinks).toContainEqual({
129
+ rel: 'alternate',
130
+ href: 'https://plone.org/es',
131
+ hrefLang: 'es',
132
+ });
133
+ expect(helmetLinks).toContainEqual({
134
+ rel: 'alternate',
135
+ href: 'https://plone.org/en',
136
+ hrefLang: 'en',
137
+ });
138
+ });
139
+
140
+ it('multilingual site, with all available translations - with server URL', () => {
141
+ config.settings.publicURL = 'https://plone.org';
85
142
  config.settings.isMultilingual = true;
86
143
  config.settings.supportedLanguages = ['en', 'es', 'eu'];
87
144
  const store = mockStore({
@@ -92,13 +149,13 @@ describe('AlternateHrefLangs', () => {
92
149
  });
93
150
 
94
151
  const content = {
95
- '@id': '/en',
152
+ '@id': 'http://localhost:8080/Plone/en',
96
153
  language: { token: 'en', title: 'English' },
97
154
  '@components': {
98
155
  translations: {
99
156
  items: [
100
- { '@id': '/eu', language: 'eu' },
101
- { '@id': '/es', language: 'es' },
157
+ { '@id': 'http://localhost:8080/Plone/eu', language: 'eu' },
158
+ { '@id': 'http://localhost:8080/Plone/es', language: 'es' },
102
159
  ],
103
160
  },
104
161
  },
@@ -119,17 +176,17 @@ describe('AlternateHrefLangs', () => {
119
176
 
120
177
  expect(helmetLinks).toContainEqual({
121
178
  rel: 'alternate',
122
- href: '/eu',
179
+ href: 'https://plone.org/eu',
123
180
  hrefLang: 'eu',
124
181
  });
125
182
  expect(helmetLinks).toContainEqual({
126
183
  rel: 'alternate',
127
- href: '/es',
184
+ href: 'https://plone.org/es',
128
185
  hrefLang: 'es',
129
186
  });
130
187
  expect(helmetLinks).toContainEqual({
131
188
  rel: 'alternate',
132
- href: '/en',
189
+ href: 'https://plone.org/en',
133
190
  hrefLang: 'en',
134
191
  });
135
192
  });
@@ -27,10 +27,10 @@ export default function Image({
27
27
  // TypeScript hints for editor autocomplete :)
28
28
  /** @type {React.ImgHTMLAttributes<HTMLImageElement>} */
29
29
  const attrs = {};
30
+ attrs.className = cx(className, { responsive }) || undefined;
30
31
 
31
32
  if (!item && src) {
32
33
  attrs.src = src;
33
- attrs.className = cx(className, { responsive });
34
34
  } else {
35
35
  const isFromRealObject = !item.image_scales;
36
36
  const imageFieldWithDefault = imageField || item.image_field || 'image';
@@ -51,7 +51,6 @@ export default function Image({
51
51
  attrs.src = `${flattenToAppURL(basePath)}/${image.download}`;
52
52
  attrs.width = image.width;
53
53
  attrs.height = image.height;
54
- attrs.className = cx(className, { responsive });
55
54
 
56
55
  if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
57
56
  const sortedScales = Object.values({
@@ -155,3 +155,11 @@ test('renders an image component from a string src', () => {
155
155
  const json = component.toJSON();
156
156
  expect(json).toMatchSnapshot();
157
157
  });
158
+
159
+ test('should not render empty class attribute in img tag', () => {
160
+ const component = renderer.create(
161
+ <Image src="/image.png" alt="no class attribute" />,
162
+ );
163
+ const json = component.toJSON();
164
+ expect(json).toMatchSnapshot();
165
+ });
package/src/storybook.jsx CHANGED
@@ -1490,6 +1490,7 @@ export const FormUndoWrapper = ({
1490
1490
  onClick={() => doUndo()}
1491
1491
  aria-label="Undo"
1492
1492
  disabled={!canUndo}
1493
+ type="button"
1493
1494
  >
1494
1495
  Undo
1495
1496
  </Button>
@@ -1500,6 +1501,7 @@ export const FormUndoWrapper = ({
1500
1501
  onClick={() => doRedo()}
1501
1502
  aria-label="Redo"
1502
1503
  disabled={!canRedo}
1504
+ type="button"
1503
1505
  >
1504
1506
  Redo
1505
1507
  </Button>