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

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 (101) hide show
  1. package/.changelog.draft +3 -4
  2. package/CHANGELOG.md +232 -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 +7 -4
  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/internalChecks.test.ts +94 -0
  64. package/src/primitives/Card/Card.stories.tsx +4 -1
  65. package/src/primitives/Card/Card.test.tsx +11 -33
  66. package/src/primitives/Card/Card.tsx +33 -43
  67. package/src/primitives/IconLinkList.tsx +53 -52
  68. package/src/theme/_bgcolor-blocks-layout.scss +43 -45
  69. package/src/theme/_content.scss +12 -13
  70. package/src/theme/_export_import.scss +94 -0
  71. package/src/theme/_footer.scss +64 -19
  72. package/src/theme/_header.scss +21 -4
  73. package/src/theme/_insets.scss +1 -1
  74. package/src/theme/_layout.scss +34 -15
  75. package/src/theme/_mobile-sticky-menu.scss +92 -0
  76. package/src/theme/_search-page.scss +249 -0
  77. package/src/theme/_typo-custom.scss +16 -5
  78. package/src/theme/_variables.scss +19 -4
  79. package/src/theme/_widgets.scss +15 -27
  80. package/src/theme/blocks/_accordion.scss +11 -4
  81. package/src/theme/blocks/_grid.scss +9 -77
  82. package/src/theme/blocks/_listing.scss +60 -126
  83. package/src/theme/blocks/_search.scss +3 -4
  84. package/src/theme/blocks/_table.scss +1 -0
  85. package/src/theme/blocks/_teaser.scss +7 -117
  86. package/src/theme/blocks/error-boundary.scss +11 -0
  87. package/src/theme/card.scss +107 -70
  88. package/src/theme/main.scss +5 -0
  89. package/src/theme/notfound.scss +27 -0
  90. package/src/theme/person.scss +28 -12
  91. package/src/theme/sticky-menu.scss +7 -5
  92. package/src/types.d.ts +1 -0
  93. package/vitest.config.mjs +4 -0
  94. package/razzle.extend.js +0 -38
  95. package/src/components/Blocks/schema.js +0 -44
  96. package/src/components/Breadcrumbs/Breadcrumbs.jsx +0 -118
  97. package/src/components/Widgets/AlignWidget.tsx +0 -84
  98. package/src/components/Widgets/BlockAlignment.tsx +0 -88
  99. package/src/components/Widgets/BlockWidth.tsx +0 -101
  100. package/src/components/Widgets/Buttons.tsx +0 -144
  101. package/src/components/Widgets/Size.tsx +0 -78
@@ -0,0 +1,128 @@
1
+ import React from 'react';
2
+ import { describe, expect, it, beforeEach, vi } from 'vitest';
3
+ import { render, screen } 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 Breadcrumbs from './Breadcrumbs';
9
+ import { getBreadcrumbs } from '@plone/volto/actions/breadcrumbs/breadcrumbs';
10
+ import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
11
+ import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
12
+
13
+ vi.mock('@plone/components', () => ({
14
+ Container: ({ children }: { children: React.ReactNode }) => (
15
+ <div data-testid="container">{children}</div>
16
+ ),
17
+ }));
18
+
19
+ vi.mock('@plone/volto/components/theme/Icon/Icon', () => ({
20
+ __esModule: true,
21
+ default: () => <span data-testid="home-icon" />,
22
+ }));
23
+
24
+ vi.mock('@plone/volto/actions/breadcrumbs/breadcrumbs', () => ({
25
+ getBreadcrumbs: vi.fn(() => ({ type: 'GET_BREADCRUMBS' })),
26
+ }));
27
+
28
+ vi.mock('@plone/volto/helpers/Url/Url', () => ({
29
+ getBaseUrl: vi.fn(() => '/base'),
30
+ }));
31
+
32
+ vi.mock('@plone/volto/helpers/Utils/Utils', () => ({
33
+ hasApiExpander: vi.fn(() => false),
34
+ }));
35
+
36
+ const mockStore = configureStore<{
37
+ breadcrumbs: {
38
+ items: { title: string; url: string }[];
39
+ root?: string;
40
+ };
41
+ intl: {
42
+ locale: string;
43
+ messages: Record<string, string>;
44
+ };
45
+ }>();
46
+
47
+ const mockedGetBreadcrumbs = vi.mocked(getBreadcrumbs);
48
+ const mockedGetBaseUrl = vi.mocked(getBaseUrl);
49
+ const mockedHasApiExpander = vi.mocked(hasApiExpander);
50
+
51
+ const renderBreadcrumbs = (
52
+ options: {
53
+ pathname?: string;
54
+ items?: { title: string; url: string }[];
55
+ root?: string;
56
+ } = {},
57
+ ) => {
58
+ const {
59
+ pathname = '/current-path',
60
+ items = [
61
+ { title: 'Section one', url: '/section-one' },
62
+ { title: 'Current page', url: '/section-one/current' },
63
+ ],
64
+ root = '/home',
65
+ } = options;
66
+
67
+ const store = mockStore({
68
+ breadcrumbs: {
69
+ items,
70
+ root,
71
+ },
72
+ intl: {
73
+ locale: 'en',
74
+ messages: {},
75
+ },
76
+ });
77
+
78
+ const renderResult = render(
79
+ <Provider store={store}>
80
+ <MemoryRouter>
81
+ <Breadcrumbs pathname={pathname} />
82
+ </MemoryRouter>
83
+ </Provider>,
84
+ );
85
+
86
+ return { store, ...renderResult };
87
+ };
88
+
89
+ describe('Breadcrumbs', () => {
90
+ beforeEach(() => {
91
+ vi.clearAllMocks();
92
+ mockedGetBaseUrl.mockReturnValue('/base');
93
+ mockedGetBreadcrumbs.mockReturnValue({ type: 'GET_BREADCRUMBS' });
94
+ });
95
+
96
+ it('renders the home link and breadcrumb items', () => {
97
+ mockedHasApiExpander.mockReturnValue(true);
98
+ const { container } = renderBreadcrumbs();
99
+
100
+ const homeLink = screen.getByTitle('Home') as HTMLAnchorElement;
101
+ expect(homeLink).toHaveAttribute('href', '/home');
102
+ expect(screen.getByText('Section one')).toBeInTheDocument();
103
+ expect(screen.getByText('Current page')).toHaveClass('section', {
104
+ exact: false,
105
+ });
106
+ expect(container.querySelector('.section.active')?.textContent).toBe(
107
+ 'Current page',
108
+ );
109
+ });
110
+
111
+ it('dispatches breadcrumb fetching when no expander is registered', () => {
112
+ mockedHasApiExpander.mockReturnValue(false);
113
+ mockedGetBaseUrl.mockReturnValue('/base-url');
114
+ const { store } = renderBreadcrumbs({ pathname: '/another-path' });
115
+
116
+ expect(mockedGetBaseUrl).toHaveBeenCalledWith('/another-path');
117
+ expect(mockedGetBreadcrumbs).toHaveBeenCalledWith('/base-url');
118
+ expect(store.getActions()).toEqual([{ type: 'GET_BREADCRUMBS' }]);
119
+ });
120
+
121
+ it('skips fetching when breadcrumbs expander already exists', () => {
122
+ mockedHasApiExpander.mockReturnValue(true);
123
+ const { store } = renderBreadcrumbs();
124
+
125
+ expect(mockedGetBreadcrumbs).not.toHaveBeenCalled();
126
+ expect(store.getActions()).toHaveLength(0);
127
+ });
128
+ });
@@ -0,0 +1,117 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import {
3
+ type TypedUseSelectorHook,
4
+ useDispatch,
5
+ useSelector,
6
+ } from 'react-redux';
7
+ import { Link, useLocation } from 'react-router-dom';
8
+ import { defineMessages, useIntl } from 'react-intl';
9
+ import { Container } from '@plone/components';
10
+
11
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
12
+ import { getBreadcrumbs } from '@plone/volto/actions/breadcrumbs/breadcrumbs';
13
+ import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
14
+ import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
15
+
16
+ import homeSVG from '@plone/volto/icons/home.svg';
17
+
18
+ const messages = defineMessages({
19
+ home: {
20
+ id: 'Home',
21
+ defaultMessage: 'Home',
22
+ },
23
+ breadcrumbs: {
24
+ id: 'Breadcrumbs',
25
+ defaultMessage: 'Breadcrumbs',
26
+ },
27
+ controlpanel: {
28
+ id: 'Site Setup',
29
+ defaultMessage: 'Site Setup',
30
+ },
31
+ });
32
+
33
+ type BreadcrumbItem = {
34
+ title: string;
35
+ url: string;
36
+ };
37
+
38
+ type BreadcrumbsProps = {
39
+ pathname: string;
40
+ };
41
+
42
+ type BreadcrumbState = {
43
+ items: BreadcrumbItem[];
44
+ root?: string | null;
45
+ };
46
+
47
+ type RootState = {
48
+ breadcrumbs: BreadcrumbState;
49
+ };
50
+
51
+ const useRootSelector: TypedUseSelectorHook<RootState> = useSelector;
52
+
53
+ export const BreadcrumbsComponent = ({ pathname }: BreadcrumbsProps) => {
54
+ const intl = useIntl();
55
+ const dispatch = useDispatch();
56
+ const { pathname: realPath } = useLocation();
57
+ const controlpanelItems = useMemo(
58
+ () => [
59
+ {
60
+ url: '/controlpanel',
61
+ title: intl.formatMessage(messages.controlpanel),
62
+ },
63
+ ],
64
+ [intl],
65
+ );
66
+ const storeItems = useRootSelector((state) => state.breadcrumbs.items);
67
+ const root = useRootSelector((state) => state.breadcrumbs.root);
68
+ const items = realPath.startsWith('/controlpanel')
69
+ ? controlpanelItems
70
+ : storeItems;
71
+
72
+ useEffect(() => {
73
+ const baseUrl = getBaseUrl(pathname);
74
+
75
+ if (!hasApiExpander('breadcrumbs', baseUrl)) {
76
+ dispatch(getBreadcrumbs(baseUrl));
77
+ }
78
+ }, [dispatch, pathname]);
79
+
80
+ return (
81
+ <div
82
+ role="navigation"
83
+ aria-label={intl.formatMessage(messages.breadcrumbs)}
84
+ className="breadcrumbs"
85
+ >
86
+ <Container layout>
87
+ <div className="breadcrumb">
88
+ <Link
89
+ to={root || '/'}
90
+ className="home"
91
+ title={intl.formatMessage(messages.home)}
92
+ >
93
+ <Icon name={homeSVG} size="25px" />
94
+ </Link>
95
+ {items.map((item, index) => {
96
+ const isLast = index === items.length - 1;
97
+
98
+ return (
99
+ <React.Fragment key={item.url}>
100
+ <div className="divider" />
101
+ {isLast ? (
102
+ <div className="section active">{item.title}</div>
103
+ ) : (
104
+ <Link to={item.url} className="section">
105
+ {item.title}
106
+ </Link>
107
+ )}
108
+ </React.Fragment>
109
+ );
110
+ })}
111
+ </div>
112
+ </Container>
113
+ </div>
114
+ );
115
+ };
116
+
117
+ export default BreadcrumbsComponent;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { describe, it, expect } from 'vitest';
4
+
5
+ import Caption from './Caption';
6
+
7
+ describe('Caption', () => {
8
+ it('renders title, description lines, and credits', () => {
9
+ const { container } = render(
10
+ <Caption
11
+ title="My image"
12
+ description={'First line\n\nThird line'}
13
+ credit="Photo: Someone"
14
+ />,
15
+ );
16
+
17
+ expect(screen.getByText('My image')).toBeInTheDocument();
18
+ const paragraphs = Array.from(
19
+ container.querySelectorAll('.description p'),
20
+ ).map((node) => node.textContent);
21
+ expect(paragraphs).toEqual(['First line', '\u00A0', 'Third line']);
22
+ expect(screen.getByText('Photo: Someone')).toHaveClass('credits');
23
+ });
24
+
25
+ it('allows configuring the wrapper tag', () => {
26
+ const { container } = render(<Caption as="div" title="Custom tag" />);
27
+ const wrapper = container.querySelector('div');
28
+ expect(wrapper).not.toBeNull();
29
+ expect(wrapper?.tagName).toBe('DIV');
30
+ });
31
+ });
@@ -1,9 +1,9 @@
1
- /**
2
- * Image/video caption component.
3
- * @module components/Image/Caption
4
- */
5
- import React from 'react';
6
- import PropTypes from 'prop-types';
1
+ type CaptionProps = {
2
+ as?: keyof JSX.IntrinsicElements;
3
+ title?: string;
4
+ description?: string;
5
+ credit?: string;
6
+ };
7
7
 
8
8
  /**
9
9
  * Image/video caption component class.
@@ -14,10 +14,14 @@ import PropTypes from 'prop-types';
14
14
  * @params {object} credit Credit text.
15
15
  * @returns {string} Markup of the component.
16
16
  */
17
- const Caption = ({ as = 'figcaption', title, description, credit }) => {
18
- const As = as;
17
+ const Caption = ({
18
+ as: Tag = 'figcaption',
19
+ title,
20
+ description,
21
+ credit,
22
+ }: CaptionProps) => {
19
23
  return (
20
- <As>
24
+ <Tag>
21
25
  {title && <strong className="title">{title}</strong>}
22
26
  {description && (
23
27
  <div className="description">
@@ -27,19 +31,8 @@ const Caption = ({ as = 'figcaption', title, description, credit }) => {
27
31
  </div>
28
32
  )}
29
33
  {credit && <p className="credits">{credit}</p>}
30
- </As>
34
+ </Tag>
31
35
  );
32
36
  };
33
37
 
34
- /**
35
- * Property types.
36
- * @property {Object} propTypes Property types.
37
- * @static
38
- */
39
- Caption.propTypes = {
40
- credit: PropTypes.string,
41
- title: PropTypes.string,
42
- description: PropTypes.string,
43
- };
44
-
45
38
  export default Caption;
@@ -14,8 +14,8 @@ const ColumnLinks = (props: ColumnLinksProps) => {
14
14
  ? links.map((link) => {
15
15
  if (isEmpty(link) || !link.href) return null;
16
16
 
17
- const title = link.title || link.href[0]['title'];
18
- const href = flattenToAppURL(link.href[0]?.['@id']);
17
+ const title = link?.title || link?.href[0]?.['title'];
18
+ const href = flattenToAppURL(link?.href[0]?.['@id']);
19
19
 
20
20
  if (!href) return null;
21
21
 
@@ -2,8 +2,11 @@ import { Container } from '@plone/components';
2
2
  import Copyright from './Copyright';
3
3
  import Logo from '../../Logo/Logo';
4
4
  import type { Content } from '@plone/types';
5
+ import isString from 'lodash/isString';
5
6
  import { useLiveData } from '@kitconcept/volto-light-theme/helpers/useLiveData';
6
7
  import type { SiteFooterSettings, SiteHeaderSettings } from '../../../types';
8
+ import config from '@plone/volto/registry';
9
+ import { createParagraph } from '@plone/volto-slate/utils';
7
10
 
8
11
  const Colophon = ({ content }: { content: Content }) => {
9
12
  const logo = useLiveData<SiteHeaderSettings['logo']>(
@@ -12,14 +15,23 @@ const Colophon = ({ content }: { content: Content }) => {
12
15
  'logo',
13
16
  );
14
17
 
18
+ const getValue = (value) => {
19
+ // Previously this was a text field
20
+ if (isString(value)) {
21
+ return [createParagraph(value)];
22
+ }
23
+ return value;
24
+ };
25
+
15
26
  const footer_colophon_text = useLiveData<
16
27
  SiteFooterSettings['footer_colophon_text']
17
28
  >(content, 'voltolighttheme.footer', 'footer_colophon_text');
29
+ const RenderSlateToHtml = config.widgets.views.widget.slate_richtext;
18
30
 
19
31
  return (
20
32
  <Container className="colophon">
21
33
  {footer_colophon_text ? (
22
- <>{footer_colophon_text}</>
34
+ <RenderSlateToHtml value={getValue(footer_colophon_text)} />
23
35
  ) : (
24
36
  <>
25
37
  <div className="powered-by">
@@ -38,8 +38,10 @@ const CoreFooter = ({ content }: { content: Content }) => {
38
38
  <Container className="default">
39
39
  <div className="footer-grid">
40
40
  <div className="address-column">
41
- {/* @ts-ignore the Logo definition is different from the Volto one */}
42
- <Logo isFooterLogo />
41
+ <div className="logo">
42
+ {/* @ts-ignore the Logo definition is different from the Volto one */}
43
+ <Logo isFooterLogo />
44
+ </div>
43
45
  <p
44
46
  style={{ whiteSpace: 'pre-line' }}
45
47
  dangerouslySetInnerHTML={{
@@ -82,7 +82,7 @@ const InternetHeader = ({ pathname, content }) => {
82
82
  header_actions.map((item) => (
83
83
  <UniversalLink
84
84
  key={item['@id']}
85
- href={item.href?.[0]['@id']}
85
+ href={item.href?.[0]?.['@id']}
86
86
  openLinkInNewTab={item.openInNewTab}
87
87
  >
88
88
  {item.title}
@@ -162,7 +162,7 @@ const IntranetHeader = ({ pathname, content }) => {
162
162
 
163
163
  return (
164
164
  <>
165
- <div className="header">
165
+ <div className="header header-intranet">
166
166
  <div className="tools-wrapper">
167
167
  <LanguageSelector />
168
168
 
@@ -174,7 +174,7 @@ const IntranetHeader = ({ pathname, content }) => {
174
174
  header_actions.map((item) => (
175
175
  <UniversalLink
176
176
  key={item['@id']}
177
- href={item.href?.[0]['@id']}
177
+ href={item.href?.[0]?.['@id']}
178
178
  openLinkInNewTab={item.openInNewTab}
179
179
  >
180
180
  {item.title}
@@ -0,0 +1,91 @@
1
+ import { Link } from 'react-router-dom';
2
+
3
+ import { useSelector } from 'react-redux';
4
+ import cx from 'classnames';
5
+
6
+ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
7
+ import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
8
+ import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
9
+ import { toReactIntlLang } from '@plone/volto/helpers/Utils/Utils';
10
+
11
+ import { defineMessages, useIntl, type IntlShape } from 'react-intl';
12
+ import type {
13
+ Content,
14
+ GetSiteResponse,
15
+ GetTranslationResponse,
16
+ } from '@plone/types';
17
+
18
+ const messages = defineMessages({
19
+ switchLanguageTo: {
20
+ id: 'Switch to',
21
+ defaultMessage: 'Switch to',
22
+ },
23
+ });
24
+
25
+ type FormState = {
26
+ content: {
27
+ data: Content;
28
+ };
29
+ intl: IntlShape;
30
+ site: {
31
+ data: GetSiteResponse;
32
+ };
33
+ };
34
+
35
+ const LanguageSelector = ({
36
+ onClickAction = () => {},
37
+ }: {
38
+ onClickAction?: () => void;
39
+ }) => {
40
+ const intl = useIntl();
41
+ const currentLang = useSelector<FormState, IntlShape['locale']>(
42
+ (state) => state.intl.locale,
43
+ );
44
+ const translations = useSelector<
45
+ FormState,
46
+ GetTranslationResponse['items'] | undefined
47
+ >((state) => state.content.data?.['@components']?.translations?.items);
48
+ const isMultilingual = useSelector<
49
+ FormState,
50
+ GetSiteResponse['features']['multilingual']
51
+ >((state) => state.site.data.features?.multilingual);
52
+ const availableLanguages = useSelector<
53
+ FormState,
54
+ GetSiteResponse['plone.available_languages']
55
+ >((state) => state.site.data?.['plone.available_languages']);
56
+
57
+ return isMultilingual ? (
58
+ <div className="language-selector">
59
+ {availableLanguages?.map((lang) => {
60
+ const langKey = lang as keyof typeof langmap;
61
+ const translation = translations?.find(
62
+ (t: { language: string }) => t.language === lang,
63
+ );
64
+ return (
65
+ <Link
66
+ aria-label={`${intl.formatMessage(
67
+ messages.switchLanguageTo,
68
+ )} ${(langmap[langKey]?.nativeName || langKey).toLowerCase()}`}
69
+ className={cx({ selected: toReactIntlLang(lang) === currentLang })}
70
+ to={translation ? flattenToAppURL(translation['@id']) : `/${lang}`}
71
+ title={langmap[langKey]?.nativeName || langKey}
72
+ onClick={() => {
73
+ onClickAction();
74
+ }}
75
+ key={`language-selector-${lang}`}
76
+ >
77
+ {/* START CUSTOMIZATION */}
78
+ {langKey.slice(0, 2)}
79
+ {/* END CUSTOMIZATION} */}
80
+ </Link>
81
+ );
82
+ })}
83
+ </div>
84
+ ) : (
85
+ <Helmet>
86
+ <html lang={toReactIntlLang(currentLang)} />
87
+ </Helmet>
88
+ );
89
+ };
90
+
91
+ export default LanguageSelector;
@@ -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