@eeacms/volto-eea-design-system 1.39.1 → 1.40.1

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 CHANGED
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.40.1](https://github.com/eea/volto-eea-design-system/compare/1.40.0...1.40.1) - 4 December 2025
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix(header): tests to return empty string when number isn't found [David Ichim - [`6285df3`](https://github.com/eea/volto-eea-design-system/commit/6285df34627462e2fe0f986de9aaf6ff3b89f9f0)]
12
+ - fix(mega-menu): add column class even if value is already string [David Ichim - [`0df0db2`](https://github.com/eea/volto-eea-design-system/commit/0df0db28ec3cf214a4420aa3f853125dc529bb00)]
13
+
14
+ #### :nail_care: Enhancements
15
+
16
+ - change(test): jest config to fix cleanup package path [David Ichim - [`243a95f`](https://github.com/eea/volto-eea-design-system/commit/243a95fa1138a3e4f30f196d33ba6ebf73cd864a)]
17
+
18
+ #### :hammer_and_wrench: Others
19
+
20
+ - add rule for eeacms/countup to jest-config.js [David Ichim - [`59244c2`](https://github.com/eea/volto-eea-design-system/commit/59244c2e85447766e9e2a051e080652d93f67458)]
21
+ - Add Sonarqube tag using bise-frontend addons list [EEA Jenkins - [`314266b`](https://github.com/eea/volto-eea-design-system/commit/314266bd8131b8ade427f23f81a084714f0104af)]
22
+ ### [1.40.0](https://github.com/eea/volto-eea-design-system/compare/1.39.1...1.40.0) - 4 November 2025
23
+
7
24
  ### [1.39.1](https://github.com/eea/volto-eea-design-system/compare/1.39.0...1.39.1) - 22 September 2025
8
25
 
9
26
  #### :boom: Breaking Change
@@ -421,7 +421,9 @@ module.exports = {
421
421
  '@plone/volto-quanta/(.*)$': '<rootDir>/src/addons/volto-quanta/src/$1',
422
422
  '@eeacms/search/(.*)$': '<rootDir>/src/addons/volto-searchlib/searchlib/$1',
423
423
  '@eeacms/search': '<rootDir>/src/addons/volto-searchlib/searchlib',
424
+ '@eeacms/countup': '<rootDir>/node_modules/@eeacms/countup/lib',
424
425
  '@eeacms/(.*?)/(.*)$': '<rootDir>/node_modules/@eeacms/$1/src/$2',
426
+ '@eeacms/(.*?)$': '<rootDir>/node_modules/@eeacms/$1/src',
425
427
  '@plone/volto-slate$':
426
428
  '<rootDir>/node_modules/@plone/volto/packages/volto-slate/src',
427
429
  '@plone/volto-slate/(.*)$':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-design-system",
3
- "version": "1.39.1",
3
+ "version": "1.40.1",
4
4
  "description": "@eeacms/volto-eea-design-system: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -131,6 +131,8 @@ Default.args = {
131
131
  tablet: 4,
132
132
  computer: 4,
133
133
  },
134
+ width: 270,
135
+ height: 100,
134
136
  },
135
137
  {
136
138
  url: 'https://www.eionet.europa.eu/',
@@ -142,6 +144,8 @@ Default.args = {
142
144
  tablet: 4,
143
145
  computer: 4,
144
146
  },
147
+ width: 330,
148
+ height: 100,
145
149
  },
146
150
  ],
147
151
  social: [
@@ -27,6 +27,9 @@ const SubFooter = (props) => {
27
27
  src={manager.src}
28
28
  alt={manager.alt}
29
29
  loading="lazy"
30
+ width={manager.width}
31
+ height={manager.height}
32
+ className="footer-logo-icon"
30
33
  ></Image>
31
34
  </a>
32
35
  </div>
@@ -343,6 +343,9 @@ const Main = ({
343
343
  <Image
344
344
  src={!searchIsActive ? `${searchIcon}` : `${closeIcon}`}
345
345
  alt="Global search"
346
+ height={45}
347
+ width={45}
348
+ className="header-search-icon"
346
349
  />
347
350
  </button>
348
351
  )}
@@ -11,6 +11,7 @@ import {
11
11
  import { cloneDeep, kebabCase } from 'lodash';
12
12
 
13
13
  import { useClickOutside } from '@eeacms/volto-eea-design-system/helpers';
14
+ import { numbersToMenuItemColumns } from '../Header/utils';
14
15
 
15
16
  const generateCssClassFromUrl = (url) => {
16
17
  if (!url) return '';
@@ -135,19 +136,20 @@ export const StandardMegaMenuGrid = ({ menuItem, renderMenuItem, layout }) => {
135
136
  const urlClass = sectionItem?.url
136
137
  ? generateCssClassFromUrl(sectionItem.url)
137
138
  : '';
138
- const classNames = `${layout.menuItemColumns[columnIndex]}${
139
- urlClass ? ` ${urlClass}` : ''
140
- }`;
141
-
139
+ const classNames = `${numbersToMenuItemColumns(
140
+ layout.menuItemColumns[columnIndex],
141
+ )}${urlClass ? ` ${urlClass}` : ''}`;
142
142
  return (
143
- <div className={classNames} key={columnIndex}>
143
+ <div className={classNames} key={columnIndex + '-column'}>
144
144
  {columnIndex !== menuItemColumnsLength
145
145
  ? renderColumnContent(menuItem.items[columnIndex], columnIndex)
146
146
  : menuItem.items
147
147
  .slice(menuItemColumnsLength)
148
- .map((section, _idx) =>
149
- renderColumnContent(section, columnIndex),
150
- )}
148
+ .map((section, _idx) => (
149
+ <React.Fragment key={`${columnIndex}-${_idx}`}>
150
+ {renderColumnContent(section, columnIndex)}
151
+ </React.Fragment>
152
+ ))}
151
153
  </div>
152
154
  );
153
155
  })}
@@ -247,7 +249,7 @@ const SecondLevelContent = ({ element, topics = false, renderMenuItem }) => {
247
249
  (element) => element.title === 'At a glance',
248
250
  );
249
251
  const inDepth = element.items.find(
250
- (element) => element.url.indexOf('in-depth') !== -1,
252
+ (element) => element.url && element.url.indexOf('in-depth') !== -1,
251
253
  );
252
254
  content = (
253
255
  <List>
@@ -53,3 +53,28 @@ export function isMenuItemActive(menuItem, bestMatchUrl, bestScore) {
53
53
  const itemUrl = menuItem['@id'] || menuItem.url;
54
54
  return itemUrl === bestMatchUrl;
55
55
  }
56
+
57
+ // Helper functions for menuItemColumns conversion (numbers to semantic UI format)
58
+ export const numberToColumnString = (num) => {
59
+ const numbers = [
60
+ '',
61
+ 'one',
62
+ 'two',
63
+ 'three',
64
+ 'four',
65
+ 'five',
66
+ 'six',
67
+ 'seven',
68
+ 'eight',
69
+ 'nine',
70
+ ];
71
+ return numbers[num] ? `${numbers[num]} wide column` : '';
72
+ };
73
+
74
+ export const numbersToMenuItemColumns = (numbers) => {
75
+ // Handle both single number and array of numbers for column dimensions
76
+ if (!Array.isArray(numbers)) return numberToColumnString(numbers);
77
+ return numbers
78
+ .map((num) => numberToColumnString(parseInt(num)))
79
+ .filter((col) => col !== '');
80
+ };
@@ -2,7 +2,12 @@
2
2
  * Tests for Header utility functions
3
3
  */
4
4
 
5
- import { findBestMatchingMenuItem, isMenuItemActive } from './utils';
5
+ import {
6
+ findBestMatchingMenuItem,
7
+ isMenuItemActive,
8
+ numberToColumnString,
9
+ numbersToMenuItemColumns,
10
+ } from './utils';
6
11
 
7
12
  describe('Header utils', () => {
8
13
  describe('findBestMatchingMenuItem', () => {
@@ -120,4 +125,94 @@ describe('Header utils', () => {
120
125
  expect(result).toBe(true);
121
126
  });
122
127
  });
128
+
129
+ describe('numberToColumnString', () => {
130
+ test('converts number 1 to "one wide column"', () => {
131
+ expect(numberToColumnString(1)).toBe('one wide column');
132
+ });
133
+
134
+ test('converts number 2 to "two wide column"', () => {
135
+ expect(numberToColumnString(2)).toBe('two wide column');
136
+ });
137
+
138
+ test('converts number 3 to "three wide column"', () => {
139
+ expect(numberToColumnString(3)).toBe('three wide column');
140
+ });
141
+
142
+ test('converts number 4 to "four wide column"', () => {
143
+ expect(numberToColumnString(4)).toBe('four wide column');
144
+ });
145
+
146
+ test('converts number 5 to "five wide column"', () => {
147
+ expect(numberToColumnString(5)).toBe('five wide column');
148
+ });
149
+
150
+ test('converts number 6 to "six wide column"', () => {
151
+ expect(numberToColumnString(6)).toBe('six wide column');
152
+ });
153
+
154
+ test('converts number 7 to "seven wide column"', () => {
155
+ expect(numberToColumnString(7)).toBe('seven wide column');
156
+ });
157
+
158
+ test('converts number 8 to "eight wide column"', () => {
159
+ expect(numberToColumnString(8)).toBe('eight wide column');
160
+ });
161
+
162
+ test('converts number 9 to "nine wide column"', () => {
163
+ expect(numberToColumnString(9)).toBe('nine wide column');
164
+ });
165
+
166
+ test('returns empty string for number 0', () => {
167
+ expect(numberToColumnString(0)).toBe('');
168
+ });
169
+
170
+ test('returns empty string for numbers > 9', () => {
171
+ expect(numberToColumnString(10)).toBe('');
172
+ expect(numberToColumnString(100)).toBe('');
173
+ });
174
+
175
+ test('returns empty string for negative numbers', () => {
176
+ expect(numberToColumnString(-1)).toBe('');
177
+ });
178
+ });
179
+
180
+ describe('numbersToMenuItemColumns', () => {
181
+ test('converts array of numbers to column strings', () => {
182
+ const result = numbersToMenuItemColumns([1, 2, 3]);
183
+ expect(result).toEqual([
184
+ 'one wide column',
185
+ 'two wide column',
186
+ 'three wide column',
187
+ ]);
188
+ });
189
+
190
+ test('filters out invalid numbers (0, negative, > 9)', () => {
191
+ const result = numbersToMenuItemColumns([0, 1, 2, 10, -1, 3]);
192
+ expect(result).toEqual([
193
+ 'one wide column',
194
+ 'two wide column',
195
+ 'three wide column',
196
+ ]);
197
+ });
198
+
199
+ test('handles string numbers by parsing them', () => {
200
+ const result = numbersToMenuItemColumns(['1', '2', '3']);
201
+ expect(result).toEqual([
202
+ 'one wide column',
203
+ 'two wide column',
204
+ 'three wide column',
205
+ ]);
206
+ });
207
+
208
+ test('handles empty array', () => {
209
+ const result = numbersToMenuItemColumns([]);
210
+ expect(result).toEqual([]);
211
+ });
212
+
213
+ test('handles array with all invalid numbers', () => {
214
+ const result = numbersToMenuItemColumns([0, 10, -1, 100]);
215
+ expect(result).toEqual([]);
216
+ });
217
+ });
123
218
  });
@@ -13,15 +13,26 @@ import { Image } from 'semantic-ui-react';
13
13
  * @param {Object} intl Intl object
14
14
  * @returns {string} Markup of the component.
15
15
  */
16
- const Logo = ({ src, invertedSrc, id, url, alt, title, inverted }) => {
16
+ const Logo = ({
17
+ src,
18
+ invertedSrc,
19
+ id,
20
+ url,
21
+ alt,
22
+ title,
23
+ inverted,
24
+ width,
25
+ height,
26
+ }) => {
17
27
  return (
18
- <Link to={url} title={title} className={'logo'}>
28
+ <Link to={url} title={title} className="logo">
19
29
  <Image
20
30
  src={inverted ? invertedSrc : src}
21
31
  alt={alt}
22
- title={title}
23
32
  className="eea-logo"
24
33
  id={id}
34
+ width={width}
35
+ height={height}
25
36
  />
26
37
  </Link>
27
38
  );
@@ -137,4 +137,6 @@ Logo.args = {
137
137
  src: LogoImage,
138
138
  invertedSrc: InvertedLogoImage,
139
139
  inverted: false,
140
+ width: 350,
141
+ height: 130,
140
142
  };
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Icon } from 'semantic-ui-react';
3
3
  import { ConditionalLink } from '@plone/volto/components';
4
- import { getFieldURL } from '@plone/volto/helpers';
4
+ import { getFieldURL } from '@plone/volto/helpers/Url/Url';
5
5
 
6
6
  function Tag({ href, children }) {
7
7
  return (
@@ -1,6 +1,16 @@
1
1
  import React from 'react';
2
2
  import Tag from './Tag';
3
3
  import { Container } from 'semantic-ui-react';
4
+ import configureStore from 'redux-mock-store';
5
+ import { Provider } from 'react-redux';
6
+ const mockStore = configureStore();
7
+
8
+ const store = mockStore({
9
+ intl: {
10
+ locale: 'en',
11
+ messages: {},
12
+ },
13
+ });
4
14
 
5
15
  export default {
6
16
  title: 'Components/Tag',
@@ -15,9 +25,11 @@ export default {
15
25
  };
16
26
 
17
27
  export const Default = (args) => (
18
- <Container>
19
- <Tag href={args.link}>{args.title}</Tag>
20
- </Container>
28
+ <Provider store={store}>
29
+ <Container>
30
+ <Tag href={args.link}>{args.title}</Tag>
31
+ </Container>
32
+ </Provider>
21
33
  );
22
34
 
23
35
  Default.args = {
@@ -2,6 +2,16 @@ import React from 'react';
2
2
  import TagList from './TagList';
3
3
  import Tag from '../Tag/Tag';
4
4
  import { Container } from 'semantic-ui-react';
5
+ import configureStore from 'redux-mock-store';
6
+ import { Provider } from 'react-redux';
7
+ const mockStore = configureStore();
8
+
9
+ const store = mockStore({
10
+ intl: {
11
+ locale: 'en',
12
+ messages: {},
13
+ },
14
+ });
5
15
 
6
16
  export default {
7
17
  title: 'Components/Tag',
@@ -40,18 +50,20 @@ export default {
40
50
  };
41
51
 
42
52
  export const List = (args) => (
43
- <Container>
44
- <TagList className={args.direction}>
45
- <TagList.Title>{args.title}</TagList.Title>
46
- <TagList.Content>
47
- {args.tags.map((tag, index) => [
48
- <Tag href={tag.href} key={index}>
49
- {tag.category}
50
- </Tag>,
51
- ])}
52
- </TagList.Content>
53
- </TagList>
54
- </Container>
53
+ <Provider store={store}>
54
+ <Container>
55
+ <TagList className={args.direction}>
56
+ <TagList.Title>{args.title}</TagList.Title>
57
+ <TagList.Content>
58
+ {args.tags.map((tag, index) => [
59
+ <Tag href={tag.href} key={index}>
60
+ {tag.category}
61
+ </Tag>,
62
+ ])}
63
+ </TagList.Content>
64
+ </TagList>
65
+ </Container>
66
+ </Provider>
55
67
  );
56
68
 
57
69
  List.args = {
@@ -66,6 +66,10 @@ footer .footer-header {
66
66
  width: @subFooterItemWidth;
67
67
  height: @subFooterItemHeight;
68
68
  align-items: @subFooterItemAlignItems;
69
+ .footer-logo-icon {
70
+ width: 100%;
71
+ height: auto;
72
+ }
69
73
  }
70
74
 
71
75
  .subfooter .item .ui.grid {
@@ -307,6 +307,7 @@
307
307
  width: @logoWidth;
308
308
  max-width: @mobileLogoMaxWidth;
309
309
  margin-top: @mobileLogoMarginTop;
310
+ height: @logoHeight;
310
311
  }
311
312
 
312
313
  /* Subsite */
@@ -476,6 +477,10 @@
476
477
  img {
477
478
  width: 75%;
478
479
  }
480
+ .header-search-icon {
481
+ width: @searchIconWidthMobile;
482
+ height: @searchIconHeightMobile;
483
+ }
479
484
  // icon to be used
480
485
  i.icon {
481
486
  display: flex;
@@ -505,6 +510,10 @@
505
510
  .search-action {
506
511
  width: @tabletActionsBoxWidth;
507
512
  height: @tabletMainSectionHeight;
513
+ .header-search-icon {
514
+ width: @searchIconWidth;
515
+ height: @searchIconHeight;
516
+ }
508
517
  }
509
518
 
510
519
  /*----------------------------------------------------------------------------
@@ -115,6 +115,7 @@
115
115
 
116
116
  /* Logo */
117
117
  @logoWidth: 100%;
118
+ @logoHeight: auto;
118
119
  @mobileLogoMaxWidth: 142px;
119
120
  @tabletLogoMaxWidth: 252px;
120
121
  @computerLogoMaxWidth: 347px; //348 rendered fuzzy :(
@@ -161,6 +162,10 @@
161
162
  @tabletActionsBoxWidth: 66px;
162
163
  @computerActionsBoxWidth: 72px;
163
164
  @burgerActionBackgroundColor: @darkMidnightBlue;
165
+ @searchIconWidth: 45px;
166
+ @searchIconHeight: 45px;
167
+ @searchIconHeightMobile: 24px;
168
+ @searchIconWidthMobile: 24px;
164
169
 
165
170
  /* Mega menu and Search popup */
166
171
  @mobilePopupMarginTop: 5vh;
@@ -9,6 +9,21 @@ body {
9
9
  font-size: @baseFontSize;
10
10
  }
11
11
 
12
+ .is-fullscreen-mode {
13
+
14
+ #header,
15
+ #footer,
16
+ .skiplinks-wrapper,
17
+ .inpage-navigation-wrapper {
18
+ display: none !important;
19
+ visibility: hidden;
20
+ }
21
+
22
+ body {
23
+ margin-top: 0 !important;
24
+ }
25
+ }
26
+
12
27
  /* sensible image defaults */
13
28
  img {
14
29
  max-width: 100%;
@@ -215,11 +230,37 @@ a {
215
230
  }
216
231
  }
217
232
 
233
+ @first-print-page: ~"@page :first { margin-top: 0; }";
234
+
218
235
  @media print {
219
236
 
220
237
  @page {
221
238
  size: A4;
222
- margin: 1cm;
239
+ margin: 14mm 0;
240
+ }
241
+
242
+ @{first-print-page} html,
243
+ body {
244
+ width: auto;
245
+ height: auto;
246
+ background: @white;
247
+ color: @textColor;
248
+ }
249
+
250
+ // avoid cutting elements
251
+ body {
252
+ margin: 0;
253
+ overflow: visible !important;
254
+ }
255
+
256
+ h1,
257
+ h2,
258
+ h3,
259
+ h4 {
260
+ break-after: avoid-page;
261
+ break-inside: avoid;
262
+ page-break-after: avoid;
263
+ page-break-inside: avoid;
223
264
  }
224
265
 
225
266
  * {
@@ -227,12 +268,22 @@ a {
227
268
  print-color-adjust: exact;
228
269
  }
229
270
 
271
+ .eea.header,
272
+ .eea.header .container,
273
+ .light-header .container {
274
+ margin-top: 0 !important;
275
+ padding-top: 0 !important;
276
+ }
277
+
230
278
  .skiplinks-wrapper,
231
279
  .slider-arrow,
232
280
  #footer,
233
281
  #inpage-navigation,
234
282
  .column.actions,
235
- .slick-arrows {
283
+ .slick-arrows,
284
+ .ui.sticky,
285
+ .has-side-nav .content-box::before,
286
+ .light-header::before {
236
287
  display: none !important;
237
288
  visibility: hidden;
238
289
  }
@@ -242,6 +293,14 @@ a {
242
293
  break-inside: avoid;
243
294
  }
244
295
 
296
+ // avoid single lines at the beginning or end of a page
297
+ #page-document p,
298
+ #page-document li,
299
+ #page-document blockquote {
300
+ orphans: 3;
301
+ widows: 3;
302
+ }
303
+
245
304
  .ui.segment.breadcrumbs {
246
305
  padding: 0;
247
306
 
@@ -267,10 +326,38 @@ a {
267
326
  .embed-tableau,
268
327
  .embed-map-visualization,
269
328
  .embed-visualization {
329
+ break-inside: avoid;
270
330
  page-break-inside: avoid;
271
331
  }
272
332
 
333
+ .ui.segment,
334
+ .content-box,
335
+ .card,
336
+ figure {
337
+ box-shadow: none !important;
338
+ }
339
+
273
340
  .grid-block-teaser .card {
274
341
  page-break-inside: auto;
275
342
  }
343
+
344
+ // pdf printing optimizations
345
+ .light-header .container {
346
+ padding-left: 8mm;
347
+ padding-right: 8mm;
348
+ }
349
+
350
+ .content-type {
351
+ --text-color-secondary: @textColor;
352
+ }
353
+
354
+ .columns-view {
355
+ .ui.column.grid>[class*="three wide"].column {
356
+ width: 16.66666667% !important;
357
+ }
358
+
359
+ .ui.column.grid>[class*="ten wide"].column {
360
+ width: 83.33333333% !important;
361
+ }
362
+ }
276
363
  }