@kitconcept/volto-light-theme 8.0.0-alpha.3 → 8.0.0-alpha.30

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 (200) hide show
  1. package/.changelog.draft +6 -9
  2. package/CHANGELOG.md +310 -0
  3. package/locales/af/LC_MESSAGES/volto.po +645 -0
  4. package/locales/ar/LC_MESSAGES/volto.po +645 -0
  5. package/locales/bg/LC_MESSAGES/volto.po +645 -0
  6. package/locales/bn/LC_MESSAGES/volto.po +645 -0
  7. package/locales/ca/LC_MESSAGES/volto.po +645 -0
  8. package/locales/cs/LC_MESSAGES/volto.po +645 -0
  9. package/locales/cy/LC_MESSAGES/volto.po +645 -0
  10. package/locales/da/LC_MESSAGES/volto.po +645 -0
  11. package/locales/de/LC_MESSAGES/volto.po +83 -167
  12. package/locales/el/LC_MESSAGES/volto.po +645 -0
  13. package/locales/en/LC_MESSAGES/volto.po +30 -115
  14. package/locales/en_AU/LC_MESSAGES/volto.po +645 -0
  15. package/locales/en_GB/LC_MESSAGES/volto.po +645 -0
  16. package/locales/eo/LC_MESSAGES/volto.po +645 -0
  17. package/locales/es/LC_MESSAGES/volto.po +75 -160
  18. package/locales/et/LC_MESSAGES/volto.po +645 -0
  19. package/locales/eu/LC_MESSAGES/volto.po +59 -125
  20. package/locales/fa/LC_MESSAGES/volto.po +645 -0
  21. package/locales/fi/LC_MESSAGES/volto.po +645 -0
  22. package/locales/fr/LC_MESSAGES/volto.po +645 -0
  23. package/locales/fu/LC_MESSAGES/volto.po +645 -0
  24. package/locales/ga/LC_MESSAGES/volto.po +645 -0
  25. package/locales/gl/LC_MESSAGES/volto.po +645 -0
  26. package/locales/he/LC_MESSAGES/volto.po +645 -0
  27. package/locales/hi/LC_MESSAGES/volto.po +645 -0
  28. package/locales/hr/LC_MESSAGES/volto.po +645 -0
  29. package/locales/hu/LC_MESSAGES/volto.po +645 -0
  30. package/locales/hy/LC_MESSAGES/volto.po +645 -0
  31. package/locales/id/LC_MESSAGES/volto.po +645 -0
  32. package/locales/it/LC_MESSAGES/volto.po +645 -0
  33. package/locales/ja/LC_MESSAGES/volto.po +645 -0
  34. package/locales/ka/LC_MESSAGES/volto.po +645 -0
  35. package/locales/kn/LC_MESSAGES/volto.po +645 -0
  36. package/locales/ko/LC_MESSAGES/volto.po +645 -0
  37. package/locales/lt/LC_MESSAGES/volto.po +645 -0
  38. package/locales/lv/LC_MESSAGES/volto.po +645 -0
  39. package/locales/mi/LC_MESSAGES/volto.po +645 -0
  40. package/locales/mk_MK/LC_MESSAGES/volto.po +645 -0
  41. package/locales/ms/LC_MESSAGES/volto.po +645 -0
  42. package/locales/mt/LC_MESSAGES/volto.po +645 -0
  43. package/locales/my/LC_MESSAGES/volto.po +645 -0
  44. package/locales/nl/LC_MESSAGES/volto.po +645 -0
  45. package/locales/nl_BE/LC_MESSAGES/volto.po +645 -0
  46. package/locales/nn/LC_MESSAGES/volto.po +645 -0
  47. package/locales/no/LC_MESSAGES/volto.po +645 -0
  48. package/locales/pl/LC_MESSAGES/volto.po +645 -0
  49. package/locales/pt/LC_MESSAGES/volto.po +645 -0
  50. package/locales/pt_BR/LC_MESSAGES/volto.po +38 -123
  51. package/locales/rm/LC_MESSAGES/volto.po +645 -0
  52. package/locales/ro/LC_MESSAGES/volto.po +645 -0
  53. package/locales/ru/LC_MESSAGES/volto.po +645 -0
  54. package/locales/sk/LC_MESSAGES/volto.po +645 -0
  55. package/locales/sl/LC_MESSAGES/volto.po +645 -0
  56. package/locales/sm/LC_MESSAGES/volto.po +645 -0
  57. package/locales/sq/LC_MESSAGES/volto.po +645 -0
  58. package/locales/sr/LC_MESSAGES/volto.po +645 -0
  59. package/locales/sr_Cyrl/LC_MESSAGES/volto.po +645 -0
  60. package/locales/sr_Latn/LC_MESSAGES/volto.po +645 -0
  61. package/locales/sv/LC_MESSAGES/volto.po +645 -0
  62. package/locales/sw/LC_MESSAGES/volto.po +645 -0
  63. package/locales/ta/LC_MESSAGES/volto.po +645 -0
  64. package/locales/te/LC_MESSAGES/volto.po +645 -0
  65. package/locales/th/LC_MESSAGES/volto.po +645 -0
  66. package/locales/tl/LC_MESSAGES/volto.po +645 -0
  67. package/locales/to/LC_MESSAGES/volto.po +645 -0
  68. package/locales/tr/LC_MESSAGES/volto.po +645 -0
  69. package/locales/uk/LC_MESSAGES/volto.po +645 -0
  70. package/locales/vi/LC_MESSAGES/volto.po +645 -0
  71. package/locales/volto.pot +31 -116
  72. package/locales/zh_CN/LC_MESSAGES/volto.po +645 -0
  73. package/locales/zh_HK/LC_MESSAGES/volto.po +645 -0
  74. package/locales/zh_TW/LC_MESSAGES/volto.po +645 -0
  75. package/package.json +7 -4
  76. package/src/__mocks__/semantic-ui-react.ts +31 -0
  77. package/src/components/Blocks/Block/EditBlockWrapper.jsx +9 -3
  78. package/src/components/Blocks/Button/schema.js +12 -0
  79. package/src/components/Blocks/EventCalendar/Search/components/EventTemplate.tsx +1 -1
  80. package/src/components/Blocks/Image/Edit.jsx +9 -32
  81. package/src/components/Blocks/Image/View.jsx +9 -26
  82. package/src/components/Blocks/Image/adapter.js +28 -14
  83. package/src/components/Blocks/Image/adapter.test.js +156 -0
  84. package/src/components/Blocks/Image/schema.js +21 -7
  85. package/src/components/Blocks/Listing/DefaultTemplate.jsx +12 -6
  86. package/src/components/Blocks/Listing/GridTemplate.jsx +17 -7
  87. package/src/components/Blocks/Listing/ListingBody.jsx +4 -1
  88. package/src/components/Blocks/Listing/SummaryTemplate.jsx +17 -7
  89. package/src/components/Blocks/Maps/MapsSidebar.jsx +68 -0
  90. package/src/components/Blocks/Maps/View.jsx +37 -0
  91. package/src/components/Blocks/Maps/adapter.js +27 -0
  92. package/src/components/Blocks/Maps/adapter.test.js +63 -0
  93. package/src/components/Blocks/Maps/schema.js +42 -2
  94. package/src/components/Blocks/Separator/schema.js +12 -0
  95. package/src/components/Blocks/Teaser/DefaultBody.tsx +35 -6
  96. package/src/components/Blocks/Video/VideoSidebar.jsx +68 -0
  97. package/src/components/Blocks/Video/View.jsx +38 -0
  98. package/src/components/Blocks/Video/adapter.js +28 -0
  99. package/src/components/Blocks/Video/adapter.test.js +63 -0
  100. package/src/components/Blocks/Video/schema.js +42 -2
  101. package/src/components/Blocks/schema.ts +69 -0
  102. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +128 -0
  103. package/src/components/Breadcrumbs/Breadcrumbs.tsx +117 -0
  104. package/src/components/Caption/Caption.test.tsx +31 -0
  105. package/src/components/Caption/{Caption.jsx → Caption.tsx} +14 -21
  106. package/src/components/Footer/ColumnLinks.tsx +2 -2
  107. package/src/components/Footer/Footer.tsx +2 -2
  108. package/src/components/Footer/slots/Colophon.tsx +13 -1
  109. package/src/components/Footer/slots/CoreFooter.tsx +4 -2
  110. package/src/components/Footer/slots/FollowUsLogoAndLinks.tsx +12 -23
  111. package/src/components/Header/Header.tsx +3 -3
  112. package/src/components/LanguageSelector/LanguageSelector.tsx +91 -0
  113. package/src/components/MobileNavigation/MobileNavigation.jsx +11 -9
  114. package/src/components/Navigation/Navigation.test.tsx +176 -0
  115. package/src/components/Navigation/{Navigation.jsx → Navigation.tsx} +89 -42
  116. package/src/components/StickyMenu/MobileCarouselArrowButton.tsx +81 -0
  117. package/src/components/StickyMenu/MobileStickyMenu.tsx +76 -0
  118. package/src/components/Summary/DefaultSummary.tsx +10 -3
  119. package/src/components/Summary/EventSummary.tsx +10 -3
  120. package/src/components/Summary/FileSummary.tsx +10 -3
  121. package/src/components/Summary/NewsItemSummary.tsx +10 -3
  122. package/src/components/Summary/PersonSummary.tsx +10 -3
  123. package/src/components/Summary/Summary.stories.tsx +46 -30
  124. package/src/components/Tags/Tags.test.tsx +71 -0
  125. package/src/components/Tags/{Tags.jsx → Tags.tsx} +9 -25
  126. package/src/components/Theme/EventView.jsx +4 -4
  127. package/src/components/Theme/ImageView.jsx +8 -1
  128. package/src/components/Theme/NewsItemView.jsx +4 -4
  129. package/src/components/Theme/RenderBlocksV2.jsx +38 -15
  130. package/src/components/Widgets/ColorSwatch.stories.tsx +197 -0
  131. package/src/components/Widgets/ColorSwatch.test.tsx +188 -0
  132. package/src/components/Widgets/ColorSwatch.tsx +77 -39
  133. package/src/components/Widgets/ObjectList.tsx +37 -27
  134. package/src/components/Widgets/SoftTextWidget.tsx +129 -0
  135. package/src/components/Widgets/SoftTextareaWidget.tsx +118 -0
  136. package/src/components/Widgets/ThemeColorSwatch.tsx +5 -9
  137. package/src/config/blocks.tsx +83 -28
  138. package/src/config/classExtenders.ts +11 -10
  139. package/src/config/settings.ts +6 -0
  140. package/src/config/slots.ts +7 -0
  141. package/src/config/widgets.ts +5 -9
  142. package/src/customizations/volto/components/manage/Blocks/Maps/MapsSidebar.jsx +10 -0
  143. package/src/customizations/volto/components/manage/Blocks/Maps/View.jsx +10 -0
  144. package/src/customizations/volto/components/manage/Blocks/Video/VideoSidebar.jsx +10 -0
  145. package/src/customizations/volto/components/manage/Blocks/Video/View.jsx +10 -0
  146. package/src/customizations/volto/components/manage/DragDropList/DragDropList.jsx +263 -0
  147. package/src/customizations/volto/components/theme/LanguageSelector/LanguageSelector.tsx +10 -0
  148. package/src/helpers/styleDefinitions.test.tsx +30 -0
  149. package/src/helpers/styleDefinitions.ts +49 -0
  150. package/src/helpers/useLiveData.ts +7 -2
  151. package/src/index.ts +15 -0
  152. package/src/internalChecks.test.ts +94 -0
  153. package/src/primitives/Card/Card.stories.tsx +4 -1
  154. package/src/primitives/Card/Card.test.tsx +11 -33
  155. package/src/primitives/Card/Card.tsx +37 -44
  156. package/src/primitives/IconLinkList.tsx +53 -52
  157. package/src/primitives/LinkIconButton.tsx +52 -0
  158. package/src/reducers/errorContext.ts +14 -0
  159. package/src/reducers/index.ts +7 -0
  160. package/src/theme/_bgcolor-blocks-layout.scss +48 -46
  161. package/src/theme/_content.scss +12 -13
  162. package/src/theme/_export_import.scss +94 -0
  163. package/src/theme/_footer.scss +131 -64
  164. package/src/theme/_header.scss +25 -5
  165. package/src/theme/_insets.scss +1 -1
  166. package/src/theme/_layout.scss +41 -77
  167. package/src/theme/_mobile-sticky-menu.scss +92 -0
  168. package/src/theme/_search-page.scss +250 -0
  169. package/src/theme/_typo-custom.scss +24 -8
  170. package/src/theme/_variables.scss +40 -4
  171. package/src/theme/_widgets.scss +6 -17
  172. package/src/theme/blocks/_accordion.scss +11 -4
  173. package/src/theme/blocks/_form.scss +350 -0
  174. package/src/theme/blocks/_grid.scss +10 -77
  175. package/src/theme/blocks/_highlight.scss +10 -7
  176. package/src/theme/blocks/_image.scss +99 -184
  177. package/src/theme/blocks/_listing.scss +61 -128
  178. package/src/theme/blocks/_maps.scss +60 -34
  179. package/src/theme/blocks/_search.scss +3 -4
  180. package/src/theme/blocks/_table.scss +1 -0
  181. package/src/theme/blocks/_teaser.scss +7 -117
  182. package/src/theme/blocks/_toc.scss +2 -1
  183. package/src/theme/card.scss +136 -69
  184. package/src/theme/main.scss +4 -0
  185. package/src/theme/notfound.scss +2 -0
  186. package/src/theme/person.scss +7 -1
  187. package/src/theme/sticky-menu.scss +7 -5
  188. package/src/transforms/to6.ts +5 -49
  189. package/src/transforms/to8.test.js +201 -0
  190. package/src/transforms/to8.ts +109 -0
  191. package/src/types.d.ts +1 -0
  192. package/vitest.config.mjs +28 -3
  193. package/razzle.extend.js +0 -38
  194. package/src/components/Blocks/schema.js +0 -44
  195. package/src/components/Breadcrumbs/Breadcrumbs.jsx +0 -118
  196. package/src/components/Widgets/AlignWidget.tsx +0 -84
  197. package/src/components/Widgets/BlockAlignment.tsx +0 -88
  198. package/src/components/Widgets/BlockWidth.tsx +0 -101
  199. package/src/components/Widgets/Buttons.tsx +0 -167
  200. package/src/components/Widgets/Size.tsx +0 -78
@@ -0,0 +1,30 @@
1
+ import { styleDefinitionsEnhancer } from './styleDefinitions';
2
+ import config from '@plone/volto/registry';
3
+
4
+ describe('styleDefinitionsEnhancer', () => {
5
+ it('should enhance style definitions correctly', () => {
6
+ const data = {
7
+ styles: {
8
+ backgroundColor: 'red',
9
+ textColor: 'blue',
10
+ },
11
+ };
12
+
13
+ config.registerUtility({
14
+ type: 'styleFieldDefinition',
15
+ name: 'backgroundColor',
16
+ method: () => {
17
+ return [
18
+ { name: 'red', label: 'Red', style: { '--bg-color': 'red' } },
19
+ { name: 'green', label: 'Green', style: { '--bg-color': 'green' } },
20
+ ];
21
+ },
22
+ });
23
+
24
+ const container = {};
25
+
26
+ const result = styleDefinitionsEnhancer({ data, container });
27
+
28
+ expect(result).toEqual({ '--bg-color': 'red' });
29
+ });
30
+ });
@@ -0,0 +1,49 @@
1
+ import config from '@plone/volto/registry';
2
+ import { findStyleByName } from '@plone/volto/helpers/Blocks/Blocks';
3
+ import isEmpty from 'lodash/isEmpty';
4
+
5
+ export function blockThemesEnhancer({ data, container }) {
6
+ if (!data['@type']) return {};
7
+ const blockConfig = config.blocks.blocksConfig[data['@type']];
8
+ if (!blockConfig) return {};
9
+ const blockStyleDefinitions =
10
+ // We look up for the blockThemes in the block's config, then in the global config
11
+ // We keep `colors` for BBB, but `themes` should be used
12
+ blockConfig.themes || blockConfig.colors || config.blocks.themes || [];
13
+
14
+ if (
15
+ !isEmpty(container) &&
16
+ container.theme &&
17
+ (!data.theme || data.theme === 'default')
18
+ ) {
19
+ return findStyleByName(blockStyleDefinitions, container.theme);
20
+ }
21
+ if (data.theme) {
22
+ return data.theme ? findStyleByName(blockStyleDefinitions, data.theme) : {};
23
+ } else {
24
+ // No theme, return default color
25
+ return findStyleByName(config.blocks.themes, 'default');
26
+ }
27
+ }
28
+
29
+ export function styleDefinitionsEnhancer({ data, container }) {
30
+ let resultantStyles = {};
31
+ Object.keys(data.styles || {}).forEach((fieldName) => {
32
+ const styleFieldEnhancer = config.getUtility({
33
+ type: 'styleFieldDefinition',
34
+ name: fieldName,
35
+ });
36
+
37
+ if (styleFieldEnhancer.method) {
38
+ resultantStyles = {
39
+ ...resultantStyles,
40
+ ...findStyleByName(
41
+ styleFieldEnhancer.method({ data, container }),
42
+ data.styles[fieldName],
43
+ ),
44
+ };
45
+ }
46
+ });
47
+
48
+ return resultantStyles;
49
+ }
@@ -6,6 +6,7 @@ type FormState = {
6
6
  content: {
7
7
  data: Content;
8
8
  };
9
+ errorContext: Content;
9
10
  form: {
10
11
  global: Content;
11
12
  };
@@ -16,11 +17,15 @@ export function useLiveData<T>(
16
17
  behavior: string | undefined,
17
18
  field: string,
18
19
  ) {
20
+ const errorContext = useSelector((state: FormState) => state.errorContext);
21
+ const context = content ?? errorContext;
22
+
19
23
  const location = useLocation();
20
24
  const addMode = location?.pathname?.endsWith('/add');
25
+
21
26
  const current = behavior
22
- ? (content?.['@components']?.inherit?.[behavior]?.data?.[field] as T)
23
- : (content[field] as T);
27
+ ? (context?.['@components']?.inherit?.[behavior]?.data?.[field] as T)
28
+ : (context[field] as T);
24
29
 
25
30
  const formData = useSelector<FormState, T>(
26
31
  (state) => state.form.global?.[field],
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import { Container } from '@plone/components';
6
6
  import EventView from './components/Theme/EventView';
7
7
 
8
8
  import { migrateToVLT6ColorAndWidthModel } from './transforms/to6';
9
+ import { migrateToVLT8FloatingBlocks } from './transforms/to8';
9
10
 
10
11
  import installSettings from './config/settings';
11
12
  import installBlocks from './config/blocks';
@@ -14,6 +15,8 @@ import installWidgets from './config/widgets';
14
15
  import installSlots from './config/slots';
15
16
  import installSummary from './config/summary';
16
17
 
18
+ import reducers from './reducers';
19
+
17
20
  import '@plone/components/dist/basic.css';
18
21
 
19
22
  import type {
@@ -96,8 +99,20 @@ const applyConfig = (config: ConfigType) => {
96
99
  method: migrateToVLT6ColorAndWidthModel,
97
100
  });
98
101
 
102
+ config.registerUtility({
103
+ name: 'migrateToVLT8FloatingBlocks',
104
+ type: 'transform',
105
+ dependencies: { reducer: 'content' },
106
+ method: migrateToVLT8FloatingBlocks,
107
+ });
108
+
99
109
  config.views.contentTypesViews.Event = EventView;
100
110
 
111
+ config.addonReducers = {
112
+ ...config.addonReducers,
113
+ ...reducers,
114
+ };
115
+
101
116
  return config;
102
117
  };
103
118
 
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const recommendedAddonsPath = path.resolve(
10
+ __dirname,
11
+ '../../../..',
12
+ 'recommendedAddons.json',
13
+ );
14
+ const mrsDeveloperPath = path.resolve(
15
+ __dirname,
16
+ '../../..',
17
+ 'mrs.developer.json',
18
+ );
19
+
20
+ const readJSON = <T>(filePath: string): T =>
21
+ JSON.parse(fs.readFileSync(filePath, 'utf-8'));
22
+
23
+ const normalizeVersion = (version: unknown) =>
24
+ typeof version === 'string' ? version.replace(/^[\\^~]/, '') : version;
25
+
26
+ const excludedPackages = new Set(['@eeacms/volto-accordion-block']);
27
+
28
+ describe('internal checks', () => {
29
+ it('keeps recommended add-ons in sync with mrs.developer.json', () => {
30
+ const recommendedAddons = readJSON<Record<string, string>>(
31
+ recommendedAddonsPath,
32
+ );
33
+ const mrsDeveloper = readJSON<Record<string, unknown>>(mrsDeveloperPath);
34
+
35
+ const mrsByPackage = Object.entries(mrsDeveloper).reduce<
36
+ Record<string, Record<string, unknown> & { configKey: string }>
37
+ >((acc, [configKey, configValue]) => {
38
+ if (
39
+ configValue &&
40
+ typeof configValue === 'object' &&
41
+ 'package' in configValue
42
+ ) {
43
+ acc[String((configValue as Record<string, unknown>).package)] = {
44
+ ...(configValue as Record<string, unknown>),
45
+ configKey,
46
+ };
47
+ }
48
+ return acc;
49
+ }, {});
50
+
51
+ const issues: string[] = [];
52
+
53
+ Object.entries(recommendedAddons).forEach(
54
+ ([packageName, recommendedVersion]) => {
55
+ if (excludedPackages.has(packageName)) return;
56
+
57
+ const mrsEntry = mrsByPackage[packageName];
58
+
59
+ if (!mrsEntry) {
60
+ issues.push(
61
+ `${packageName} is listed in recommendedAddons.json but missing from frontend/mrs.developer.json.`,
62
+ );
63
+ return;
64
+ }
65
+
66
+ const mrsVersion =
67
+ (mrsEntry.tag as string | undefined) ||
68
+ (mrsEntry.branch as string | undefined) ||
69
+ (mrsEntry.version as string | undefined);
70
+
71
+ if (!mrsVersion) {
72
+ issues.push(
73
+ `${packageName} (${mrsEntry.configKey}) has no tag, branch, or version specified in frontend/mrs.developer.json.`,
74
+ );
75
+ return;
76
+ }
77
+
78
+ const normalizedRecommended = normalizeVersion(recommendedVersion);
79
+ const normalizedMrsVersion = normalizeVersion(mrsVersion);
80
+
81
+ if (
82
+ recommendedVersion !== mrsVersion &&
83
+ normalizedRecommended !== normalizedMrsVersion
84
+ ) {
85
+ issues.push(
86
+ `${packageName} differs: recommendedAddons.json=${recommendedVersion} vs frontend/mrs.developer.json(${mrsEntry.configKey})=${mrsVersion}.`,
87
+ );
88
+ }
89
+ },
90
+ );
91
+
92
+ expect(issues).toEqual([]);
93
+ });
94
+ });
@@ -165,7 +165,10 @@ export const CustomImage: Story = {
165
165
  render: (args) => (
166
166
  <div
167
167
  className="card-listing"
168
- style={{ width: 'var(--default-container-width)' }}
168
+ style={{
169
+ width: 'var(--default-container-width)',
170
+ '--theme-high-contrast-color': '#ecebeb',
171
+ }}
169
172
  >
170
173
  <Card href={args.href}>
171
174
  <Card.Image>
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { describe, it, expect, vi } from 'vitest';
3
- import { render, fireEvent } from '@testing-library/react';
3
+ import { render } from '@testing-library/react';
4
4
  import Card from './Card';
5
5
 
6
6
  vi.mock(
@@ -43,10 +43,16 @@ vi.mock(
43
43
 
44
44
  type SummaryProps = {
45
45
  a11yLabelId?: string;
46
+ LinkToItem?: React.ElementType;
46
47
  };
47
48
 
48
- const SummaryContent = ({ a11yLabelId }: SummaryProps) => (
49
- <h3 id={a11yLabelId}>Card title</h3>
49
+ const SummaryContent = ({
50
+ a11yLabelId,
51
+ LinkToItem = React.Fragment,
52
+ }: SummaryProps) => (
53
+ <h3 id={a11yLabelId}>
54
+ <LinkToItem>Card title</LinkToItem>
55
+ </h3>
50
56
  );
51
57
 
52
58
  const BodyContent = () => <div>Body content</div>;
@@ -64,26 +70,20 @@ describe('Card', () => {
64
70
 
65
71
  it('is interactive when an href is provided', () => {
66
72
  const { container } = renderCard({ href: '/target', className: 'custom' });
67
- const card = container.querySelector('.card') as HTMLElement;
68
-
69
- expect(card).toHaveAttribute('role', 'link');
70
- expect(card).toHaveAttribute('tabindex', '0');
71
73
 
72
74
  const anchor = container.querySelector('a');
73
75
  expect(anchor).not.toBeNull();
74
76
  expect(anchor).toHaveAttribute('href', '/target');
77
+ expect(anchor).toHaveClass('card-primary-link');
75
78
  });
76
79
 
77
80
  it('is interactive when an item is provided', () => {
78
81
  const { container } = renderCard({ item: { '@id': '/item-target' } });
79
- const card = container.querySelector('.card') as HTMLElement;
80
-
81
- expect(card).toHaveAttribute('role', 'link');
82
- expect(card).toHaveAttribute('tabindex', '0');
83
82
 
84
83
  const anchor = container.querySelector('a');
85
84
  expect(anchor).not.toBeNull();
86
85
  expect(anchor).toHaveAttribute('href', '/item-target');
86
+ expect(anchor).toHaveClass('card-primary-link');
87
87
  });
88
88
 
89
89
  it('is not interactive when neither href nor item is provided', () => {
@@ -112,26 +112,4 @@ describe('Card', () => {
112
112
  expect(card).not.toHaveAttribute('tabindex');
113
113
  expect(container.querySelector('a')).toBeNull();
114
114
  });
115
-
116
- it('triggers navigation handlers when interactive', () => {
117
- const clickSpy = vi
118
- .spyOn(HTMLAnchorElement.prototype, 'click')
119
- .mockImplementation(() => {});
120
- const selectionSpy = vi
121
- .spyOn(window, 'getSelection')
122
- .mockReturnValue({ toString: () => '' } as unknown as Selection);
123
-
124
- const { container } = renderCard({ href: '/target' });
125
- const card = container.querySelector('.card') as HTMLElement;
126
-
127
- fireEvent.click(card);
128
- fireEvent.keyDown(card, { key: 'Enter' });
129
- fireEvent.keyDown(card, { key: ' ' });
130
- fireEvent.keyDown(card, { key: 'Escape' });
131
-
132
- expect(clickSpy).toHaveBeenCalledTimes(3);
133
-
134
- clickSpy.mockRestore();
135
- selectionSpy.mockRestore();
136
- });
137
115
  });
@@ -52,49 +52,32 @@ const Card = (props: CardProps) => {
52
52
  const { className, openLinkInNewTab } = props;
53
53
 
54
54
  const a11yLabelId = React.useId();
55
- const linkRef = React.useRef<HTMLAnchorElement>(null);
56
-
57
- const triggerNavigation = () => {
58
- // Only navigate if there is *no* text selection
59
- const hasSelection = !!window.getSelection()?.toString();
60
- if (!hasSelection) {
61
- linkRef.current?.click();
62
- }
63
- };
64
-
65
55
  const isInteractive = !!props.href || !!props.item;
66
56
 
67
- const onClick: React.MouseEventHandler<HTMLDivElement> = () => {
68
- if (isInteractive) triggerNavigation();
69
- };
70
-
71
- const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
72
- if (!isInteractive) return;
73
- if (e.key === 'Enter' || e.key === ' ') {
74
- e.preventDefault();
75
- triggerNavigation();
76
- }
77
- };
57
+ const LinkToItem = React.useCallback(
58
+ ({ children }: { children: React.ReactNode }) => {
59
+ return (
60
+ <ConditionalLink
61
+ className="card-primary-link"
62
+ condition={isInteractive}
63
+ href={href}
64
+ item={item}
65
+ openLinkInNewTab={openLinkInNewTab}
66
+ >
67
+ {children}
68
+ </ConditionalLink>
69
+ );
70
+ },
71
+ [href, item, isInteractive, openLinkInNewTab],
72
+ );
78
73
 
79
74
  return (
80
- <div
81
- className={cx('card', className)}
82
- onClick={isInteractive ? onClick : undefined}
83
- onKeyDown={isInteractive ? onKeyDown : undefined}
84
- role={isInteractive ? 'link' : undefined}
85
- tabIndex={isInteractive ? 0 : undefined}
86
- >
87
- {/* @ts-expect-error since this has no children, should fail */}
88
- <ConditionalLink
89
- aria-labelledby={a11yLabelId}
90
- condition={isInteractive}
91
- href={href}
92
- item={item}
93
- openLinkInNewTab={openLinkInNewTab}
94
- ref={linkRef}
95
- />
75
+ <div className={cx('card', className)}>
96
76
  <div className="card-inner">
97
- {childrenWithProps(props.children, { a11yLabelId })}
77
+ {childrenWithProps(props.children, {
78
+ a11yLabelId,
79
+ LinkToItem,
80
+ })}
98
81
  </div>
99
82
  </div>
100
83
  );
@@ -111,10 +94,12 @@ type CardImageProps = {
111
94
  imageComponent?: React.ComponentType<any>;
112
95
  children?: React.ReactNode;
113
96
  showPlaceholderImage?: boolean;
97
+ sizes?: string;
114
98
  };
115
99
 
116
100
  const CardImage = (props: CardImageProps) => {
117
- const { src, item, image, imageComponent, showPlaceholderImage } = props;
101
+ const { src, item, image, imageComponent, showPlaceholderImage, sizes } =
102
+ props;
118
103
  const Image = imageComponent || DefaultImage;
119
104
 
120
105
  return (
@@ -132,6 +117,7 @@ const CardImage = (props: CardImageProps) => {
132
117
  alt=""
133
118
  loading="lazy"
134
119
  responsive={true}
120
+ sizes={sizes}
135
121
  />
136
122
  )
137
123
  ) : (
@@ -144,14 +130,21 @@ const CardImage = (props: CardImageProps) => {
144
130
  type CardSummaryProps = {
145
131
  /** The ID of the element that labels the card. */
146
132
  a11yLabelId?: string;
133
+ LinkToItem?: React.ElementType;
147
134
  children?: React.ReactNode;
148
135
  };
149
136
 
150
- const CardSummary = (props: CardSummaryProps) => (
151
- <div className="card-summary">
152
- {childrenWithProps(props.children, { a11yLabelId: props.a11yLabelId })}
153
- </div>
154
- );
137
+ const CardSummary = (props: CardSummaryProps) => {
138
+ const { a11yLabelId, LinkToItem } = props;
139
+ return (
140
+ <div className="card-summary">
141
+ {childrenWithProps(props.children, {
142
+ a11yLabelId,
143
+ LinkToItem,
144
+ })}
145
+ </div>
146
+ );
147
+ };
155
148
 
156
149
  const CardActions = (props: any) => (
157
150
  <div className="actions-wrapper">{props.children}</div>
@@ -6,6 +6,58 @@ type IconLinkListProps = {
6
6
  iconLinks: Array<iconLink>;
7
7
  };
8
8
 
9
+ export const IconLinkListTemplate = ({ item }) => {
10
+ const itemInfo: {
11
+ title: string;
12
+ hrefTitle: string;
13
+ href: string;
14
+ itemHref: string;
15
+ src: string;
16
+ srcAlt: string;
17
+ } = {
18
+ title: '',
19
+ hrefTitle: '',
20
+ href: '',
21
+ itemHref: '',
22
+ src: '',
23
+ srcAlt: '',
24
+ };
25
+
26
+ // If the item has title, always set it
27
+ itemInfo.title = item ? item?.title || item?.href?.[0]?.['title'] || '' : '';
28
+ if (item?.href?.length > 0) {
29
+ itemInfo.title = item.title || item.href[0]['title'];
30
+ itemInfo.href = flattenToAppURL(item.href[0]['@id']);
31
+ }
32
+ if (item?.icon && item.icon[0]?.image_scales) {
33
+ itemInfo.itemHref = item.icon[0]['@id'];
34
+ itemInfo.srcAlt = item['alt'];
35
+ itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/${item.icon[0].image_scales[item.icon[0].image_field][0].download}`;
36
+ } else if (item?.icon && item.icon[0]) {
37
+ itemInfo.itemHref = item.icon[0]['@id'];
38
+ itemInfo.srcAlt = item['alt'];
39
+ itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/@@images/image`;
40
+ }
41
+
42
+ if (!itemInfo.src) return null;
43
+ return (
44
+ <li className="item" key={item['@id']}>
45
+ {/* @ts-ignore */}
46
+ <ConditionalLink
47
+ condition={itemInfo.href}
48
+ to={itemInfo.href}
49
+ title={itemInfo.hrefTitle || itemInfo.srcAlt}
50
+ openLinkInNewTab={item.openInNewTab}
51
+ >
52
+ <div className="image-wrapper">
53
+ <img src={itemInfo.src} alt={itemInfo.srcAlt || ''} />
54
+ </div>
55
+ <span className="title">{itemInfo.title}</span>
56
+ </ConditionalLink>
57
+ </li>
58
+ );
59
+ };
60
+
9
61
  const IconLinkList = (props: IconLinkListProps) => {
10
62
  const { iconLinks } = props;
11
63
 
@@ -13,58 +65,7 @@ const IconLinkList = (props: IconLinkListProps) => {
13
65
  <ul>
14
66
  {iconLinks && Array.isArray(iconLinks)
15
67
  ? iconLinks.map((item) => {
16
- const itemInfo: {
17
- title: string;
18
- hrefTitle: string;
19
- href: string;
20
- itemHref: string;
21
- src: string;
22
- srcAlt: string;
23
- } = {
24
- title: '',
25
- hrefTitle: '',
26
- href: '',
27
- itemHref: '',
28
- src: '',
29
- srcAlt: '',
30
- };
31
-
32
- // If the item has title, always set it
33
- itemInfo.title = item
34
- ? item?.title || item?.href?.[0]?.['title'] || ''
35
- : '';
36
- if (item?.href?.length > 0) {
37
- itemInfo.title = item.title || item.href[0]['title'];
38
- itemInfo.href = flattenToAppURL(item.href[0]['@id']);
39
- }
40
- if (item?.icon && item.icon[0]?.image_scales) {
41
- itemInfo.itemHref = item.icon[0]['@id'];
42
- itemInfo.srcAlt = item['alt'];
43
- itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/${item.icon[0].image_scales[item.icon[0].image_field][0].download}`;
44
- } else if (item?.icon && item.icon[0]) {
45
- itemInfo.itemHref = item.icon[0]['@id'];
46
- itemInfo.srcAlt = item['alt'];
47
- itemInfo.src = `${flattenToAppURL(itemInfo.itemHref)}/@@images/image`;
48
- }
49
-
50
- if (!itemInfo.src) return null;
51
-
52
- return (
53
- <li className="item" key={item['@id']}>
54
- {/* @ts-ignore */}
55
- <ConditionalLink
56
- condition={itemInfo.href}
57
- to={itemInfo.href}
58
- title={itemInfo.hrefTitle || itemInfo.srcAlt}
59
- openLinkInNewTab={item.openInNewTab}
60
- >
61
- <div className="image-wrapper">
62
- <img src={itemInfo.src} alt={itemInfo.srcAlt || ''} />
63
- </div>
64
- <span>{itemInfo.title}</span>
65
- </ConditionalLink>
66
- </li>
67
- );
68
+ return <IconLinkListTemplate item={item} key={item['@id']} />;
68
69
  })
69
70
  : null}
70
71
  </ul>
@@ -0,0 +1,52 @@
1
+ import { useSelector } from 'react-redux';
2
+ import { useLocation, useHistory } from 'react-router-dom';
3
+ import type { ObjectBrowserItem, GetSiteResponse } from '@plone/types';
4
+ import linkSVG from '@plone/volto/icons/link.svg';
5
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
6
+ import { Button } from '@plone/components';
7
+ import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
8
+
9
+ type FormState = {
10
+ site: { data: GetSiteResponse };
11
+ };
12
+
13
+ const useLinkIconNavigation = (item?: Partial<ObjectBrowserItem>) => {
14
+ const location = useLocation();
15
+ const history = useHistory();
16
+
17
+ const handleLinkIconClick = (e: React.MouseEvent<HTMLButtonElement>) => {
18
+ e.preventDefault();
19
+ e.stopPropagation();
20
+
21
+ const targetUrl = item?.['@id'];
22
+ if (targetUrl) {
23
+ const flattenedTargetUrl = flattenToAppURL(targetUrl);
24
+ const searchParams = new URLSearchParams();
25
+ searchParams.set('return_to', location.pathname);
26
+
27
+ history.push({
28
+ pathname: flattenedTargetUrl,
29
+ search: searchParams.toString(),
30
+ });
31
+ }
32
+ };
33
+ return handleLinkIconClick;
34
+ };
35
+ const LinkIconButton = ({ item }: { item?: Partial<ObjectBrowserItem> }) => {
36
+ const site = useSelector<FormState, GetSiteResponse>(
37
+ (state) => state.site?.data,
38
+ );
39
+ const hideProfileLinks = site?.['kitconcept.disable_profile_links'];
40
+ const handleLinkIconClick = useLinkIconNavigation(item);
41
+ return (
42
+ hideProfileLinks && (
43
+ <div className="card-link-icon">
44
+ <Button aria-label="link" onClick={handleLinkIconClick}>
45
+ <Icon name={linkSVG} size="33px" />
46
+ </Button>
47
+ </div>
48
+ )
49
+ );
50
+ };
51
+
52
+ export default LinkIconButton;
@@ -0,0 +1,14 @@
1
+ import { GET_CONTENT } from '@plone/volto/constants/ActionTypes';
2
+
3
+ const initialState = {};
4
+
5
+ export default function errorContext(state = initialState, action: any = {}) {
6
+ switch (action.type) {
7
+ case `${GET_CONTENT}_PENDING`:
8
+ return {};
9
+ case `${GET_CONTENT}_FAIL`:
10
+ return action.error.response?.body ?? {};
11
+ default:
12
+ return state;
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ import errorContext from './errorContext';
2
+
3
+ const reducers = {
4
+ errorContext,
5
+ };
6
+
7
+ export default reducers;