@kitconcept/volto-light-theme 8.0.0-alpha.2 → 8.0.0-alpha.20

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 (103) hide show
  1. package/.changelog.draft +3 -4
  2. package/CHANGELOG.md +226 -0
  3. package/locales/de/LC_MESSAGES/volto.po +30 -115
  4. package/locales/en/LC_MESSAGES/volto.po +30 -115
  5. package/locales/es/LC_MESSAGES/volto.po +31 -116
  6. package/locales/eu/LC_MESSAGES/volto.po +58 -124
  7. package/locales/pt_BR/LC_MESSAGES/volto.po +38 -123
  8. package/locales/volto.pot +31 -116
  9. package/package.json +18 -15
  10. package/src/__mocks__/semantic-ui-react.ts +31 -0
  11. package/src/components/Blocks/Block/Edit.jsx +14 -6
  12. package/src/components/Blocks/Block/EditBlockWrapper.jsx +9 -3
  13. package/src/components/Blocks/Block/ErrorBoundary.test.tsx +55 -0
  14. package/src/components/Blocks/Block/ErrorBoundary.tsx +92 -0
  15. package/src/components/Blocks/Block/ErrorBoundaryMessage.tsx +66 -0
  16. package/src/components/Blocks/EventCalendar/Search/components/EventTemplate.tsx +1 -1
  17. package/src/components/Blocks/Image/Edit.jsx +1 -0
  18. package/src/components/Blocks/Listing/DefaultTemplate.jsx +12 -6
  19. package/src/components/Blocks/Listing/GridTemplate.jsx +16 -7
  20. package/src/components/Blocks/Listing/ListingBody.jsx +4 -1
  21. package/src/components/Blocks/Listing/SummaryTemplate.jsx +16 -7
  22. package/src/components/Blocks/Teaser/DefaultBody.tsx +25 -5
  23. package/src/components/Blocks/schema.ts +69 -0
  24. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +128 -0
  25. package/src/components/Breadcrumbs/Breadcrumbs.tsx +117 -0
  26. package/src/components/Caption/Caption.test.tsx +31 -0
  27. package/src/components/Caption/{Caption.jsx → Caption.tsx} +14 -21
  28. package/src/components/Footer/ColumnLinks.tsx +2 -2
  29. package/src/components/Footer/slots/Colophon.tsx +13 -1
  30. package/src/components/Footer/slots/CoreFooter.tsx +4 -2
  31. package/src/components/Header/Header.tsx +3 -3
  32. package/src/components/LanguageSelector/LanguageSelector.tsx +91 -0
  33. package/src/components/MobileNavigation/MobileNavigation.jsx +11 -9
  34. package/src/components/Navigation/Navigation.test.tsx +176 -0
  35. package/src/components/Navigation/{Navigation.jsx → Navigation.tsx} +77 -37
  36. package/src/components/StickyMenu/MobileCarouselArrowButton.tsx +81 -0
  37. package/src/components/StickyMenu/MobileStickyMenu.tsx +76 -0
  38. package/src/components/Summary/DefaultSummary.tsx +10 -3
  39. package/src/components/Summary/EventSummary.tsx +10 -3
  40. package/src/components/Summary/FileSummary.tsx +10 -3
  41. package/src/components/Summary/NewsItemSummary.tsx +10 -3
  42. package/src/components/Summary/PersonSummary.tsx +10 -3
  43. package/src/components/Summary/Summary.stories.tsx +46 -30
  44. package/src/components/Tags/Tags.test.tsx +71 -0
  45. package/src/components/Tags/{Tags.jsx → Tags.tsx} +9 -25
  46. package/src/components/Theme/EventView.jsx +4 -4
  47. package/src/components/Theme/NewsItemView.jsx +4 -4
  48. package/src/components/Theme/RenderBlocks.jsx +45 -37
  49. package/src/components/Theme/RenderBlocksV2.jsx +51 -20
  50. package/src/components/Widgets/ColorSwatch.stories.tsx +197 -0
  51. package/src/components/Widgets/ColorSwatch.test.tsx +188 -0
  52. package/src/components/Widgets/ColorSwatch.tsx +77 -39
  53. package/src/components/Widgets/SoftTextWidget.tsx +129 -0
  54. package/src/components/Widgets/SoftTextareaWidget.tsx +118 -0
  55. package/src/components/Widgets/ThemeColorSwatch.tsx +5 -9
  56. package/src/config/blocks.tsx +21 -29
  57. package/src/config/slots.ts +7 -0
  58. package/src/config/widgets.ts +5 -9
  59. package/src/customizations/volto/components/manage/DragDropList/DragDropList.jsx +263 -0
  60. package/src/customizations/volto/components/theme/LanguageSelector/LanguageSelector.tsx +10 -0
  61. package/src/helpers/styleDefinitions.test.tsx +30 -0
  62. package/src/helpers/styleDefinitions.ts +49 -0
  63. package/src/icons/block_icn_accordion.svg +0 -0
  64. package/src/icons/block_icn_grid.svg +0 -0
  65. package/src/internalChecks.test.ts +94 -0
  66. package/src/primitives/Card/Card.stories.tsx +4 -1
  67. package/src/primitives/Card/Card.test.tsx +11 -33
  68. package/src/primitives/Card/Card.tsx +33 -43
  69. package/src/primitives/IconLinkList.tsx +53 -52
  70. package/src/theme/_bgcolor-blocks-layout.scss +43 -45
  71. package/src/theme/_content.scss +12 -13
  72. package/src/theme/_export_import.scss +94 -0
  73. package/src/theme/_footer.scss +64 -19
  74. package/src/theme/_header.scss +21 -4
  75. package/src/theme/_insets.scss +1 -1
  76. package/src/theme/_layout.scss +34 -15
  77. package/src/theme/_mobile-sticky-menu.scss +92 -0
  78. package/src/theme/_search-page.scss +249 -0
  79. package/src/theme/_typo-custom.scss +16 -5
  80. package/src/theme/_variables.scss +19 -4
  81. package/src/theme/_widgets.scss +15 -27
  82. package/src/theme/blocks/_accordion.scss +11 -4
  83. package/src/theme/blocks/_grid.scss +9 -77
  84. package/src/theme/blocks/_listing.scss +60 -126
  85. package/src/theme/blocks/_search.scss +3 -4
  86. package/src/theme/blocks/_table.scss +1 -0
  87. package/src/theme/blocks/_teaser.scss +7 -117
  88. package/src/theme/blocks/error-boundary.scss +11 -0
  89. package/src/theme/card.scss +107 -70
  90. package/src/theme/main.scss +5 -0
  91. package/src/theme/notfound.scss +27 -0
  92. package/src/theme/person.scss +28 -12
  93. package/src/theme/sticky-menu.scss +7 -5
  94. package/src/types.d.ts +1 -0
  95. package/vitest.config.mjs +4 -0
  96. package/razzle.extend.js +0 -38
  97. package/src/components/Blocks/schema.js +0 -44
  98. package/src/components/Breadcrumbs/Breadcrumbs.jsx +0 -118
  99. package/src/components/Widgets/AlignWidget.tsx +0 -84
  100. package/src/components/Widgets/BlockAlignment.tsx +0 -88
  101. package/src/components/Widgets/BlockWidth.tsx +0 -101
  102. package/src/components/Widgets/Buttons.tsx +0 -144
  103. package/src/components/Widgets/Size.tsx +0 -78
@@ -14,7 +14,7 @@ import arrowRightSVG from '@plone/volto/icons/right-key.svg';
14
14
  import arrowLeftSVG from '@plone/volto/icons/left-key.svg';
15
15
  import { MobileNavigationToggler } from './MobileNavigationToggler';
16
16
  import { MobileToolsFooter } from './MobileToolsFooter';
17
-
17
+ import { useClient } from '@plone/volto/hooks/client/useClient';
18
18
  const messages = defineMessages({
19
19
  closeMobileMenu: {
20
20
  id: 'Close menu',
@@ -43,7 +43,7 @@ const MenuItem = ({
43
43
  pathname,
44
44
  }) => {
45
45
  const [isSubMenuOpen, setSubMenuOpen] = useState(false);
46
-
46
+ const isClient = useClient();
47
47
  const openSubMenu = useCallback((e) => {
48
48
  e.stopPropagation();
49
49
  setSubMenuOpen(true);
@@ -68,9 +68,10 @@ const MenuItem = ({
68
68
  );
69
69
  const formData = useSelector((state) => state.form.global);
70
70
 
71
- const has_intranet_header = !isEmpty(formData)
72
- ? formData.has_intranet_header
73
- : headerSettings?.has_intranet_header;
71
+ const has_intranet_header =
72
+ isClient && !isEmpty(formData)
73
+ ? formData.has_intranet_header
74
+ : headerSettings?.has_intranet_header;
74
75
 
75
76
  return (
76
77
  <li className={section.url === pathname ? 'current' : ''}>
@@ -154,16 +155,17 @@ const MobileNavigation = (props) => {
154
155
  const currentLang = useSelector((state) => state.intl.locale);
155
156
  const items = useSelector((state) => state.navigation.items || []);
156
157
  const history = useHistory();
157
-
158
+ const isClient = useClient();
158
159
  const headerSettings = useSelector(
159
160
  (state) =>
160
161
  state.content.data?.['@components']?.inherit?.['voltolighttheme.header']
161
162
  ?.data,
162
163
  );
163
164
  const formData = useSelector((state) => state.form.global);
164
- const has_intranet_header = !isEmpty(formData)
165
- ? formData.has_intranet_header
166
- : headerSettings?.has_intranet_header;
165
+ const has_intranet_header =
166
+ isClient && !isEmpty(formData)
167
+ ? formData.has_intranet_header
168
+ : headerSettings?.has_intranet_header;
167
169
 
168
170
  const Footer = props.MobileToolsFooter || MobileToolsFooter;
169
171
 
@@ -0,0 +1,176 @@
1
+ import React from 'react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import configureStore from 'redux-mock-store';
5
+ import { Provider } from 'react-intl-redux';
6
+ import { MemoryRouter } from 'react-router-dom';
7
+
8
+ import Navigation from './Navigation';
9
+ import { getNavigation } from '@plone/volto/actions/navigation/navigation';
10
+ import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
11
+ import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
12
+
13
+ vi.mock('../../helpers/doesNodeContainClick', () => ({
14
+ __esModule: true,
15
+ default: () => false,
16
+ }));
17
+
18
+ vi.mock('@plone/volto/components/theme/Icon/Icon', () => ({
19
+ __esModule: true,
20
+ default: () => <span data-testid="icon" />,
21
+ }));
22
+
23
+ vi.mock('@plone/volto/components/theme/Navigation/NavItem', () => ({
24
+ __esModule: true,
25
+ default: ({ item }: { item: { title: string } }) => (
26
+ <span data-testid="nav-item">{item.title}</span>
27
+ ),
28
+ }));
29
+
30
+ vi.mock('@plone/volto/actions/navigation/navigation', () => ({
31
+ getNavigation: vi.fn(() => ({ type: 'GET_NAVIGATION' })),
32
+ }));
33
+
34
+ vi.mock('@plone/volto/helpers/Url/Url', () => ({
35
+ getBaseUrl: vi.fn((path: string) => path),
36
+ }));
37
+
38
+ vi.mock('@plone/volto/helpers/Utils/Utils', () => ({
39
+ hasApiExpander: vi.fn(() => false),
40
+ }));
41
+
42
+ vi.mock('@plone/volto/registry', () => ({
43
+ __esModule: true,
44
+ default: {
45
+ settings: {
46
+ navDepth: 3,
47
+ },
48
+ },
49
+ }));
50
+
51
+ vi.mock('@plone/volto/icons/clear.svg', () => ({
52
+ __esModule: true,
53
+ default: 'clear.svg',
54
+ }));
55
+
56
+ const mockStore = configureStore();
57
+
58
+ const mockedHasApiExpander = vi.mocked(hasApiExpander);
59
+ const mockedGetNavigation = vi.mocked(getNavigation);
60
+ const mockedGetBaseUrl = vi.mocked(getBaseUrl);
61
+
62
+ type TestNavigationItem = {
63
+ title: string;
64
+ url: string;
65
+ nav_title?: string;
66
+ items?: TestNavigationItem[];
67
+ };
68
+
69
+ const baseItems: TestNavigationItem[] = [
70
+ { title: 'About', url: '/about' },
71
+ { title: 'News', url: '/news' },
72
+ ];
73
+
74
+ const renderNavigation = ({
75
+ items = baseItems,
76
+ hasFatMenuHeader = false,
77
+ formFatMenu,
78
+ pathname = '/news',
79
+ }: {
80
+ items?: TestNavigationItem[];
81
+ hasFatMenuHeader?: boolean;
82
+ formFatMenu?: boolean;
83
+ pathname?: string;
84
+ } = {}) => {
85
+ const store = mockStore({
86
+ content: {
87
+ data: {
88
+ '@components': {
89
+ inherit: {
90
+ 'voltolighttheme.header': {
91
+ data: { has_fat_menu: hasFatMenuHeader },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ form: {
98
+ global:
99
+ formFatMenu === undefined ? undefined : { has_fat_menu: formFatMenu },
100
+ },
101
+ intl: {
102
+ locale: 'en',
103
+ },
104
+ userSession: {
105
+ token: null,
106
+ },
107
+ navigation: {
108
+ items,
109
+ },
110
+ });
111
+
112
+ const renderResult = render(
113
+ <Provider store={store}>
114
+ <MemoryRouter>
115
+ <Navigation pathname={pathname} />
116
+ </MemoryRouter>
117
+ </Provider>,
118
+ );
119
+
120
+ return { store, ...renderResult };
121
+ };
122
+
123
+ describe('Navigation', () => {
124
+ beforeEach(() => {
125
+ vi.clearAllMocks();
126
+ mockedHasApiExpander.mockReturnValue(false);
127
+ mockedGetNavigation.mockReturnValue({ type: 'GET_NAVIGATION' });
128
+ mockedGetBaseUrl.mockImplementation((path: string) => path);
129
+ });
130
+
131
+ it('renders simple nav items when fat menu is disabled', () => {
132
+ renderNavigation({ hasFatMenuHeader: false });
133
+
134
+ const navItems = screen.getAllByTestId('nav-item');
135
+ expect(navItems).toHaveLength(baseItems.length);
136
+ expect(navItems[0]).toHaveTextContent('About');
137
+ });
138
+
139
+ it('expands and collapses the fat menu', () => {
140
+ const { container } = renderNavigation({
141
+ hasFatMenuHeader: true,
142
+ items: [
143
+ {
144
+ title: 'Sections',
145
+ url: '/sections',
146
+ items: [
147
+ {
148
+ title: 'First',
149
+ url: '/sections/first',
150
+ items: [],
151
+ },
152
+ ],
153
+ },
154
+ ],
155
+ });
156
+
157
+ const openButtons = screen.getAllByRole('button', {
158
+ name: 'Open menu',
159
+ });
160
+ fireEvent.click(openButtons[0]);
161
+ expect(container.querySelector('.submenu.active')).not.toBeNull();
162
+
163
+ const closeButton = screen.getByLabelText('Close menu');
164
+ fireEvent.click(closeButton);
165
+ expect(container.querySelector('.submenu.active')).toBeNull();
166
+ });
167
+
168
+ it('fetches navigation data when no expander is present', () => {
169
+ mockedGetBaseUrl.mockReturnValue('/base');
170
+ const { store } = renderNavigation({ pathname: '/blog' });
171
+
172
+ expect(mockedGetBaseUrl).toHaveBeenCalledWith('/blog');
173
+ expect(mockedGetNavigation).toHaveBeenCalledWith('/base', 3);
174
+ expect(store.getActions()).toEqual([{ type: 'GET_NAVIGATION' }]);
175
+ });
176
+ });
@@ -1,12 +1,9 @@
1
- // SemanticUI-free pre-@plone/components
2
-
3
1
  import React, { useState, useEffect, useRef } from 'react';
4
- import PropTypes from 'prop-types';
5
2
  import isEmpty from 'lodash/isEmpty';
6
3
  import { useDispatch, useSelector, shallowEqual } from 'react-redux';
7
4
  import { NavLink } from 'react-router-dom';
8
5
  import doesNodeContainClick from '../../helpers/doesNodeContainClick';
9
- import { useIntl, defineMessages, injectIntl } from 'react-intl';
6
+ import { useIntl, defineMessages } from 'react-intl';
10
7
  import cx from 'classnames';
11
8
  import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
12
9
  import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
@@ -28,31 +25,87 @@ const messages = defineMessages({
28
25
  },
29
26
  });
30
27
 
31
- const Navigation = ({ pathname }) => {
32
- const [desktopMenuOpen, setDesktopMenuOpen] = useState(null);
33
- const [currentOpenIndex, setCurrentOpenIndex] = useState(null);
34
- const navigation = useRef(null);
28
+ type NavigationItem = {
29
+ title: string;
30
+ nav_title?: string;
31
+ url: string;
32
+ items?: NavigationItem[];
33
+ };
34
+
35
+ type NavigationProps = {
36
+ pathname: string;
37
+ };
38
+
39
+ type HeaderSettings = {
40
+ has_fat_menu?: boolean;
41
+ };
42
+
43
+ type RootState = {
44
+ content: {
45
+ data?: {
46
+ '@components'?: {
47
+ inherit?: {
48
+ 'voltolighttheme.header'?: {
49
+ data?: HeaderSettings;
50
+ };
51
+ };
52
+ };
53
+ };
54
+ };
55
+ form: {
56
+ global?: {
57
+ has_fat_menu?: boolean;
58
+ [key: string]: unknown;
59
+ };
60
+ };
61
+ intl: {
62
+ locale: string;
63
+ };
64
+ userSession: {
65
+ token?: string | null;
66
+ };
67
+ navigation: {
68
+ items: NavigationItem[];
69
+ };
70
+ };
71
+
72
+ const Navigation = ({ pathname }: NavigationProps) => {
73
+ const [desktopMenuOpen, setDesktopMenuOpen] = useState<number | null>(null);
74
+ const [currentOpenIndex, setCurrentOpenIndex] = useState<number | null>(null);
75
+ const navigation = useRef<HTMLElement | null>(null);
35
76
  const dispatch = useDispatch();
36
77
  const intl = useIntl();
37
78
  const headerSettings = useSelector(
38
- (state) =>
79
+ (state: RootState) =>
39
80
  state.content.data?.['@components']?.inherit?.['voltolighttheme.header']
40
81
  ?.data,
41
82
  );
42
- const formData = useSelector((state) => state.form.global);
83
+ const formData = useSelector((state: RootState) => state.form.global);
43
84
 
44
- const has_fat_menu =
45
- !isEmpty(formData) && formData?.has_fat_menu
85
+ const hasFatMenuSetting =
86
+ !isEmpty(formData) && formData?.has_fat_menu !== undefined
46
87
  ? formData.has_fat_menu
47
88
  : headerSettings?.has_fat_menu;
89
+ const hasFatMenu = hasFatMenuSetting ?? false;
90
+
91
+ const lang = useSelector((state: RootState) => state.intl.locale);
92
+ const token = useSelector(
93
+ (state: RootState) => state.userSession.token,
94
+ shallowEqual,
95
+ );
96
+ const items = useSelector(
97
+ (state: RootState) => state.navigation.items,
98
+ shallowEqual,
99
+ );
48
100
 
49
- const lang = useSelector((state) => state.intl.locale);
50
- const token = useSelector((state) => state.userSession.token, shallowEqual);
51
- const items = useSelector((state) => state.navigation.items, shallowEqual);
101
+ const closeMenu = () => {
102
+ setDesktopMenuOpen(null);
103
+ setCurrentOpenIndex(null);
104
+ };
52
105
 
53
106
  useEffect(() => {
54
- const handleClickOutside = (e) => {
55
- if (navigation.current && doesNodeContainClick(navigation.current, e))
107
+ const handleClickOutside = (event: MouseEvent) => {
108
+ if (navigation.current && doesNodeContainClick(navigation.current, event))
56
109
  return;
57
110
  closeMenu();
58
111
  };
@@ -70,11 +123,11 @@ const Navigation = ({ pathname }) => {
70
123
  }
71
124
  }, [pathname, token, dispatch]);
72
125
 
73
- const isActive = (url) => {
126
+ const isActive = (url: string) => {
74
127
  return (url === '' && pathname === '/') || (url !== '' && pathname === url);
75
128
  };
76
129
 
77
- const openMenu = (index) => {
130
+ const openMenu = (index: number) => {
78
131
  if (index === currentOpenIndex) {
79
132
  setDesktopMenuOpen(null);
80
133
  setCurrentOpenIndex(null);
@@ -84,14 +137,9 @@ const Navigation = ({ pathname }) => {
84
137
  }
85
138
  };
86
139
 
87
- const closeMenu = (index) => {
88
- setDesktopMenuOpen(null);
89
- setCurrentOpenIndex(null);
90
- };
91
-
92
140
  useEffect(() => {
93
- const handleEsc = (event) => {
94
- if (event.keyCode === 27) {
141
+ const handleEsc = (event: KeyboardEvent) => {
142
+ if (event.key === 'Escape' || event.keyCode === 27) {
95
143
  closeMenu();
96
144
  }
97
145
  };
@@ -113,7 +161,7 @@ const Navigation = ({ pathname }) => {
113
161
  <ul className="desktop-menu">
114
162
  {items.map((item, index) => (
115
163
  <li key={item.url}>
116
- {has_fat_menu ? (
164
+ {hasFatMenu ? (
117
165
  <>
118
166
  <button
119
167
  onClick={() => openMenu(index)}
@@ -123,7 +171,7 @@ const Navigation = ({ pathname }) => {
123
171
  (!desktopMenuOpen && pathname === item.url),
124
172
  })}
125
173
  aria-label={intl.formatMessage(messages.openFatMenu)}
126
- aria-expanded={desktopMenuOpen === index ? true : false}
174
+ aria-expanded={desktopMenuOpen === index}
127
175
  >
128
176
  {item.title}
129
177
  </button>
@@ -212,12 +260,4 @@ const Navigation = ({ pathname }) => {
212
260
  );
213
261
  };
214
262
 
215
- Navigation.propTypes = {
216
- pathname: PropTypes.string.isRequired,
217
- };
218
-
219
- Navigation.defaultProps = {
220
- token: null,
221
- };
222
-
223
- export default injectIntl(Navigation);
263
+ export default Navigation;
@@ -0,0 +1,81 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { EmblaCarouselType } from 'embla-carousel';
3
+ import type { ComponentPropsWithRef } from 'react';
4
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
5
+ import rightArrowSVG from '@plone/volto/icons/right-key.svg';
6
+ import leftArrowSVG from '@plone/volto/icons/left-key.svg';
7
+
8
+ type UsePrevNextButtonsType = {
9
+ prevBtnDisabled: boolean;
10
+ nextBtnDisabled: boolean;
11
+ onPrevButtonClick: () => void;
12
+ onNextButtonClick: () => void;
13
+ };
14
+
15
+ export const usePrevNextButtons = (
16
+ emblaApi: EmblaCarouselType | undefined,
17
+ ): UsePrevNextButtonsType => {
18
+ const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
19
+ const [nextBtnDisabled, setNextBtnDisabled] = useState(true);
20
+
21
+ const onPrevButtonClick = useCallback(() => {
22
+ if (!emblaApi) return;
23
+ emblaApi.scrollPrev();
24
+ }, [emblaApi]);
25
+
26
+ const onNextButtonClick = useCallback(() => {
27
+ if (!emblaApi) return;
28
+ emblaApi.scrollNext();
29
+ }, [emblaApi]);
30
+
31
+ const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
32
+ setPrevBtnDisabled(!emblaApi.canScrollPrev());
33
+ setNextBtnDisabled(!emblaApi.canScrollNext());
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ if (!emblaApi) return;
38
+
39
+ onSelect(emblaApi);
40
+ emblaApi.on('reInit', onSelect).on('select', onSelect);
41
+ }, [emblaApi, onSelect]);
42
+
43
+ return {
44
+ prevBtnDisabled,
45
+ nextBtnDisabled,
46
+ onPrevButtonClick,
47
+ onNextButtonClick,
48
+ };
49
+ };
50
+
51
+ type PropType = ComponentPropsWithRef<'button'>;
52
+
53
+ export const PrevButton: React.FC<PropType> = (props) => {
54
+ const { children, ...restProps } = props;
55
+
56
+ return (
57
+ <button
58
+ className="embla__button embla__button--prev"
59
+ type="button"
60
+ {...restProps}
61
+ >
62
+ <Icon name={leftArrowSVG} size="48px" />
63
+ {children}
64
+ </button>
65
+ );
66
+ };
67
+
68
+ export const NextButton: React.FC<PropType> = (props) => {
69
+ const { children, ...restProps } = props;
70
+
71
+ return (
72
+ <button
73
+ className="embla__button embla__button--next"
74
+ type="button"
75
+ {...restProps}
76
+ >
77
+ <Icon name={rightArrowSVG} size="48px" />
78
+ {children}
79
+ </button>
80
+ );
81
+ };
@@ -0,0 +1,76 @@
1
+ import { useLiveData } from '@kitconcept/volto-light-theme/helpers/useLiveData';
2
+ import { IconLinkListTemplate } from '@kitconcept/volto-light-theme/primitives/IconLinkList';
3
+ import type { StickyMenuSettings } from '../../types';
4
+ import useEmblaCarousel from 'embla-carousel-react';
5
+ import type { EmblaOptionsType } from 'embla-carousel';
6
+
7
+ import {
8
+ usePrevNextButtons,
9
+ PrevButton,
10
+ NextButton,
11
+ } from './MobileCarouselArrowButton';
12
+ import type { Content } from '@plone/types';
13
+
14
+ const MobileStickyMenu = ({ content }: { content: Content }) => {
15
+ const options: EmblaOptionsType = {};
16
+ const [emblaRef, emblaApi] = useEmblaCarousel(options);
17
+ const {
18
+ prevBtnDisabled,
19
+ nextBtnDisabled,
20
+ onPrevButtonClick,
21
+ onNextButtonClick,
22
+ } = usePrevNextButtons(emblaApi);
23
+ const showMobileStickyMenu = useLiveData<StickyMenuSettings['sticky_menu']>(
24
+ content,
25
+ 'kitconcept.sticky_menu',
26
+ 'enable_mobile_sticky_menu',
27
+ );
28
+ const menuData = useLiveData<StickyMenuSettings['sticky_menu']>(
29
+ content,
30
+ 'kitconcept.sticky_menu',
31
+ 'sticky_menu',
32
+ );
33
+
34
+ const sticky_menu_color = useLiveData<
35
+ StickyMenuSettings['sticky_menu_color']
36
+ >(content, 'kitconcept.sticky_menu', 'sticky_menu_color');
37
+
38
+ const sticky_menu_foreground_color = useLiveData<
39
+ StickyMenuSettings['sticky_menu_foreground_color']
40
+ >(content, 'kitconcept.sticky_menu', 'sticky_menu_foreground_color');
41
+
42
+ if (!showMobileStickyMenu) {
43
+ return null;
44
+ }
45
+
46
+ return (
47
+ <div
48
+ className="mobile-sticky-menu"
49
+ role="navigation"
50
+ aria-label="Mobile Sticky menu"
51
+ style={
52
+ {
53
+ '--sticky-menu-color': sticky_menu_color,
54
+ '--sticky-menu-foreground-color': sticky_menu_foreground_color,
55
+ } as React.CSSProperties
56
+ }
57
+ >
58
+ <section className="embla">
59
+ <div className="embla__viewport" ref={emblaRef}>
60
+ <div className="embla__container">
61
+ {menuData &&
62
+ menuData.map((item) => (
63
+ <ul className="embla__slide" key={item['@id']}>
64
+ <IconLinkListTemplate item={item} />
65
+ </ul>
66
+ ))}
67
+ </div>
68
+ </div>
69
+ <PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
70
+ <NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
71
+ </section>
72
+ </div>
73
+ );
74
+ };
75
+
76
+ export default MobileStickyMenu;
@@ -4,6 +4,7 @@ import { smartTextRenderer } from '../../helpers/smartText';
4
4
 
5
5
  export type DefaultSummaryProps = {
6
6
  item: Partial<ObjectBrowserItem>;
7
+ LinkToItem?: React.ElementType;
7
8
  HeadingTag?: React.ElementType;
8
9
  a11yLabelId?: string;
9
10
  hide_description?: boolean;
@@ -14,14 +15,20 @@ export type SummaryComponentType = React.ComponentType<DefaultSummaryProps> & {
14
15
  };
15
16
 
16
17
  const DefaultSummary = (props: DefaultSummaryProps) => {
17
- const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
18
+ const {
19
+ item,
20
+ LinkToItem = React.Fragment,
21
+ HeadingTag = 'div',
22
+ a11yLabelId,
23
+ hide_description,
24
+ } = props;
18
25
  return (
19
26
  <>
20
27
  {item?.head_title && <div className="headline">{item.head_title}</div>}
21
28
  <HeadingTag className="title" id={a11yLabelId}>
22
- {item.title ? item.title : item.id}
29
+ <LinkToItem>{item.title ? item.title : item.id}</LinkToItem>
23
30
  </HeadingTag>
24
- {!hide_description && (
31
+ {!hide_description && item?.description !== '' && (
25
32
  <p className="description">{smartTextRenderer(item.description)}</p>
26
33
  )}
27
34
  </>
@@ -1,3 +1,4 @@
1
+ import * as React from 'react';
1
2
  import {
2
3
  parseDateFromCatalog,
3
4
  formatDateRange,
@@ -7,7 +8,13 @@ import type { DefaultSummaryProps } from './DefaultSummary';
7
8
  import { smartTextRenderer } from '../../helpers/smartText';
8
9
 
9
10
  const EventSummary = (props: DefaultSummaryProps) => {
10
- const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
11
+ const {
12
+ item,
13
+ LinkToItem = React.Fragment,
14
+ HeadingTag = 'div',
15
+ a11yLabelId,
16
+ hide_description,
17
+ } = props;
11
18
  const start = parseDateFromCatalog(item.start);
12
19
  const end = parseDateFromCatalog(item.end);
13
20
  const headline = [
@@ -29,9 +36,9 @@ const EventSummary = (props: DefaultSummaryProps) => {
29
36
  <>
30
37
  {headline.length ? <div className="headline">{headline}</div> : null}
31
38
  <HeadingTag className="title" id={a11yLabelId}>
32
- {item.title ? item.title : item.id}
39
+ <LinkToItem>{item.title ? item.title : item.id}</LinkToItem>
33
40
  </HeadingTag>
34
- {!hide_description && (
41
+ {!hide_description && item?.description !== '' && (
35
42
  <p className="description">{smartTextRenderer(item.description)}</p>
36
43
  )}
37
44
  </>
@@ -1,3 +1,4 @@
1
+ import * as React from 'react';
1
2
  import FileType from '@kitconcept/volto-light-theme/helpers/Filetype';
2
3
  import type { DefaultSummaryProps } from './DefaultSummary';
3
4
  import { smartTextRenderer } from '../../helpers/smartText';
@@ -21,15 +22,21 @@ const FileHeadline = (props: { item: any }) => {
21
22
  };
22
23
 
23
24
  const FileSummary = (props: DefaultSummaryProps) => {
24
- const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
25
+ const {
26
+ item,
27
+ LinkToItem = React.Fragment,
28
+ HeadingTag = 'div',
29
+ a11yLabelId,
30
+ hide_description,
31
+ } = props;
25
32
 
26
33
  return (
27
34
  <>
28
35
  <FileHeadline item={item} />
29
36
  <HeadingTag className="title" id={a11yLabelId}>
30
- {item.title ? item.title : item.id}
37
+ <LinkToItem>{item.title ? item.title : item.id}</LinkToItem>
31
38
  </HeadingTag>
32
- {!hide_description && (
39
+ {!hide_description && item?.description !== '' && (
33
40
  <p className="description">{smartTextRenderer(item.description)}</p>
34
41
  )}
35
42
  </>
@@ -1,10 +1,17 @@
1
+ import * as React from 'react';
1
2
  import { parseDateFromCatalog } from '@kitconcept/volto-light-theme/helpers/dates';
2
3
  import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
3
4
  import type { DefaultSummaryProps } from './DefaultSummary';
4
5
  import { smartTextRenderer } from '../../helpers/smartText';
5
6
 
6
7
  const NewsItemSummary = (props: DefaultSummaryProps) => {
7
- const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
8
+ const {
9
+ item,
10
+ LinkToItem = React.Fragment,
11
+ HeadingTag = 'div',
12
+ a11yLabelId,
13
+ hide_description,
14
+ } = props;
8
15
 
9
16
  const effective = parseDateFromCatalog(item.effective);
10
17
  const headline = [
@@ -31,9 +38,9 @@ const NewsItemSummary = (props: DefaultSummaryProps) => {
31
38
  <>
32
39
  {headline.length ? <div className="headline">{headline}</div> : null}
33
40
  <HeadingTag className="title" id={a11yLabelId}>
34
- {item.title ? item.title : item.id}
41
+ <LinkToItem>{item.title ? item.title : item.id}</LinkToItem>
35
42
  </HeadingTag>
36
- {!hide_description && (
43
+ {!hide_description && item?.description !== '' && (
37
44
  <p className="description">{smartTextRenderer(item.description)}</p>
38
45
  )}
39
46
  </>