@kitconcept/volto-light-theme 7.2.0 → 7.3.0

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.draft CHANGED
@@ -1,14 +1,13 @@
1
- ## 7.2.0 (2025-10-01)
1
+ ## 7.3.0 (2025-10-07)
2
2
 
3
- ### Feature
3
+ ### Bugfix
4
4
 
5
- - Added smartTextRenderer helper for rendering markdown formatted links in plain text. @sneridagh [#679.1](https://github.com/kitconcept/volto-light-theme/pull/679.1)
6
- - Link support in descriptions in summaries via a custom mardown parser (smartText). @sneridagh [#679.2](https://github.com/kitconcept/volto-light-theme/pull/679.2)
5
+ - Pass down items to `Card` component, so it can pass it down to `UniversalLink`. @sneridagh [#684](https://github.com/kitconcept/volto-light-theme/pull/684)
6
+ - Added addressable classNames to FileSummary headline. @sneridagh [#686](https://github.com/kitconcept/volto-light-theme/pull/686)
7
+ - Update socialmedia add-on to 2.0.0a10 and Volto to 18.27.3. @sneridagh
7
8
 
8
- ### Bugfix
9
+ ### Internal
9
10
 
10
- - Fixed icons and spacing in calendar event range widget. @sneridagh [#680](https://github.com/kitconcept/volto-light-theme/pull/680)
11
- - Added card img 100% width for account for small images. @sneridagh [#681](https://github.com/kitconcept/volto-light-theme/pull/681)
12
- - Fixed regression for contained teasers applying a margin only meant for contained ones. @sneridagh [#683](https://github.com/kitconcept/volto-light-theme/pull/683)
11
+ - Replace "head title" with "kicker" in example content. @davisagli
13
12
 
14
13
 
package/CHANGELOG.md CHANGED
@@ -8,6 +8,18 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 7.3.0 (2025-10-07)
12
+
13
+ ### Bugfix
14
+
15
+ - Pass down items to `Card` component, so it can pass it down to `UniversalLink`. @sneridagh [#684](https://github.com/kitconcept/volto-light-theme/pull/684)
16
+ - Added addressable classNames to FileSummary headline. @sneridagh [#686](https://github.com/kitconcept/volto-light-theme/pull/686)
17
+ - Update socialmedia add-on to 2.0.0a10 and Volto to 18.27.3. @sneridagh
18
+
19
+ ### Internal
20
+
21
+ - Replace "head title" with "kicker" in example content. @davisagli
22
+
11
23
  ## 7.2.0 (2025-10-01)
12
24
 
13
25
  ### Feature
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitconcept/volto-light-theme",
3
- "version": "7.2.0",
3
+ "version": "7.3.0",
4
4
  "description": "Volto Light Theme by kitconcept",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -72,7 +72,7 @@
72
72
  "@kitconcept/volto-logos-block": "^3.0.0-alpha.1",
73
73
  "@kitconcept/volto-separator-block": "^4.2.1",
74
74
  "@kitconcept/volto-slider-block": "^6.4.0",
75
- "@plonegovbr/volto-social-media": "^2.0.0-alpha.6",
75
+ "@plonegovbr/volto-social-media": "^2.0.0-alpha.10",
76
76
  "classnames": "^2.2.6",
77
77
  "lodash": "4.17.21",
78
78
  "react": "18.2.0",
@@ -39,7 +39,7 @@ const DefaultTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
39
39
  })}
40
40
  key={item['@id']}
41
41
  >
42
- <Card href={showLink ? item['@id'] : null}>
42
+ <Card item={showLink ? item : null}>
43
43
  <Card.Summary>
44
44
  <Summary item={item} HeadingTag="h2" />
45
45
  </Card.Summary>
@@ -62,7 +62,7 @@ const GridTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
62
62
  })}
63
63
  key={item['@id']}
64
64
  >
65
- <Card href={showLink ? item['@id'] : null}>
65
+ <Card item={showLink ? item : null}>
66
66
  <ItemBodyTemplate item={item} />
67
67
  </Card>
68
68
  </div>
@@ -57,7 +57,7 @@ const SummaryTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
57
57
  })}
58
58
  key={item['@id']}
59
59
  >
60
- <Card href={showLink ? item['@id'] : null}>
60
+ <Card item={showLink ? item : null}>
61
61
  <ItemBodyTemplate item={item} />
62
62
  </Card>
63
63
  </div>
@@ -23,10 +23,7 @@ const TeaserDefaultTemplate = (props) => {
23
23
  const { '@id': id, ...filteredData } = data;
24
24
 
25
25
  return (
26
- <Card
27
- href={showLink ? href['@id'] : null}
28
- openLinkInNewTab={openLinkInNewTab}
29
- >
26
+ <Card item={showLink ? href : null} openLinkInNewTab={openLinkInNewTab}>
30
27
  <Card.Image
31
28
  src={url && !image?.image_field ? url : undefined}
32
29
  item={!data.overwrite ? href : { ...href, ...filteredData }}
@@ -2,17 +2,30 @@ import FileType from '@kitconcept/volto-light-theme/helpers/Filetype';
2
2
  import type { DefaultSummaryProps } from './DefaultSummary';
3
3
  import { smartTextRenderer } from '../../helpers/smartText';
4
4
 
5
+ const FileHeadline = (props: { item: any }) => {
6
+ const { item } = props;
7
+ const headline =
8
+ item.getObjSize || FileType(item.mime_type) || item.head_title || '';
9
+
10
+ return headline?.length === 0 ? null : (
11
+ <div className="headline">
12
+ {item.getObjSize && <span className="file-size">{item.getObjSize}</span>}
13
+ {FileType(item.mime_type) && (
14
+ <span className="file-type">{FileType(item.mime_type)}</span>
15
+ )}
16
+ {item.head_title && (
17
+ <span className="headline-content">{item.head_title}</span>
18
+ )}
19
+ </div>
20
+ );
21
+ };
22
+
5
23
  const FileSummary = (props: DefaultSummaryProps) => {
6
24
  const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
7
25
 
8
- const headline = [item.getObjSize, FileType(item.mime_type), item.head_title]
9
- .filter((x) => x)
10
- .flatMap((x) => [' | ', x])
11
- .slice(1);
12
-
13
26
  return (
14
27
  <>
15
- {headline.length ? <div className="headline">{headline}</div> : null}
28
+ <FileHeadline item={item} />
16
29
  <HeadingTag className="title" id={a11yLabelId}>
17
30
  {item.title ? item.title : item.id}
18
31
  </HeadingTag>
@@ -67,6 +67,16 @@ describe('smartTextRenderer', () => {
67
67
  expect(link).toHaveAttribute('href', '/space');
68
68
  });
69
69
 
70
+ it('renders carriage returns and newlines as line breaks', () => {
71
+ const { container } = renderWithWrapper('Line 1\r\nLine 2\nLine 3');
72
+ const brs = container.querySelectorAll('br');
73
+
74
+ expect(container).toHaveTextContent('Line 1Line 2Line 3');
75
+ expect(brs).toHaveLength(2);
76
+ expect(brs[0].nextSibling?.textContent).toBe('Line 2');
77
+ expect(brs[1].nextSibling?.textContent).toBe('Line 3');
78
+ });
79
+
70
80
  it('ignores bracketed text that is not a markdown link', () => {
71
81
  const description = 'Keep [this] but not a link.';
72
82
  const { container } = renderWithWrapper(description);
@@ -34,7 +34,18 @@ export const smartTextRenderer = (smartText) => {
34
34
 
35
35
  return parts.map((part, index) => {
36
36
  if (typeof part === 'string') {
37
- return <React.Fragment key={`text-${index}`}>{part}</React.Fragment>;
37
+ const segments = part.split(/\r\n|\r|\n/);
38
+
39
+ return (
40
+ <React.Fragment key={`text-${index}`}>
41
+ {segments.map((segment, segmentIndex) => (
42
+ <React.Fragment key={`text-${index}-${segmentIndex}`}>
43
+ {segment}
44
+ {segmentIndex < segments.length - 1 ? <br /> : null}
45
+ </React.Fragment>
46
+ ))}
47
+ </React.Fragment>
48
+ );
38
49
  }
39
50
 
40
51
  return (
@@ -67,7 +67,7 @@ export const SimpleWithoutLink: Story = {
67
67
  </Card>
68
68
  ),
69
69
  args: {
70
- href: '/folder/page',
70
+ href: undefined,
71
71
  },
72
72
  decorators: [
73
73
  (Story) => (
@@ -0,0 +1,137 @@
1
+ import React from 'react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, fireEvent } from '@testing-library/react';
4
+ import Card from './Card';
5
+
6
+ vi.mock(
7
+ '@plone/volto/components/manage/ConditionalLink/ConditionalLink',
8
+ () => {
9
+ return {
10
+ __esModule: true,
11
+ default: React.forwardRef(
12
+ (
13
+ {
14
+ condition,
15
+ href,
16
+ item,
17
+ openLinkInNewTab: _openLinkInNewTab,
18
+ children,
19
+ ...rest
20
+ }: {
21
+ condition?: boolean;
22
+ href?: string;
23
+ item?: { ['@id']?: string };
24
+ openLinkInNewTab?: boolean;
25
+ children?: React.ReactNode;
26
+ },
27
+ ref: React.ForwardedRef<HTMLAnchorElement>,
28
+ ) => {
29
+ if (!condition) return null;
30
+
31
+ const computedHref = href ?? item?.['@id'] ?? '#';
32
+
33
+ return (
34
+ <a {...rest} ref={ref} href={computedHref}>
35
+ {children}
36
+ </a>
37
+ );
38
+ },
39
+ ),
40
+ };
41
+ },
42
+ );
43
+
44
+ type SummaryProps = {
45
+ a11yLabelId?: string;
46
+ };
47
+
48
+ const SummaryContent = ({ a11yLabelId }: SummaryProps) => (
49
+ <h3 id={a11yLabelId}>Card title</h3>
50
+ );
51
+
52
+ const BodyContent = () => <div>Body content</div>;
53
+
54
+ describe('Card', () => {
55
+ const renderCard = (props: React.ComponentProps<typeof Card>) =>
56
+ render(
57
+ <Card {...props}>
58
+ <Card.Summary>
59
+ <SummaryContent />
60
+ </Card.Summary>
61
+ <BodyContent />
62
+ </Card>,
63
+ );
64
+
65
+ it('is interactive when an href is provided', () => {
66
+ 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
+
72
+ const anchor = container.querySelector('a');
73
+ expect(anchor).not.toBeNull();
74
+ expect(anchor).toHaveAttribute('href', '/target');
75
+ });
76
+
77
+ it('is interactive when an item is provided', () => {
78
+ 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
+
84
+ const anchor = container.querySelector('a');
85
+ expect(anchor).not.toBeNull();
86
+ expect(anchor).toHaveAttribute('href', '/item-target');
87
+ });
88
+
89
+ it('is not interactive when neither href nor item is provided', () => {
90
+ const { container } = renderCard({});
91
+ const card = container.querySelector('.card') as HTMLElement;
92
+
93
+ expect(card).not.toHaveAttribute('role');
94
+ expect(card).not.toHaveAttribute('tabindex');
95
+ expect(container.querySelector('a')).toBeNull();
96
+ });
97
+
98
+ it('is not interactive when neither href nor item is provided and one is null', () => {
99
+ const { container } = renderCard({ href: null });
100
+ const card = container.querySelector('.card') as HTMLElement;
101
+
102
+ expect(card).not.toHaveAttribute('role');
103
+ expect(card).not.toHaveAttribute('tabindex');
104
+ expect(container.querySelector('a')).toBeNull();
105
+ });
106
+
107
+ it('is not interactive when neither href nor item is provided and one is undefined', () => {
108
+ const { container } = renderCard({ item: undefined });
109
+ const card = container.querySelector('.card') as HTMLElement;
110
+
111
+ expect(card).not.toHaveAttribute('role');
112
+ expect(card).not.toHaveAttribute('tabindex');
113
+ expect(container.querySelector('a')).toBeNull();
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
+ });
@@ -3,16 +3,27 @@ import ConditionalLink from '@plone/volto/components/manage/ConditionalLink/Cond
3
3
  import cx from 'classnames';
4
4
  import type { ObjectBrowserItem } from '@plone/types';
5
5
 
6
- type CardProps = {
6
+ type BaseCardProps = {
7
7
  /** Optional additional CSS class names to apply to the card. */
8
8
  className?: string;
9
- /** Optional URL to make the card clickable as a link. */
10
- href?: string;
11
- /** If true and `href` is provided, opens the link in a new browser tab. */
12
9
  openLinkInNewTab?: boolean;
13
10
  children?: React.ReactNode;
14
11
  };
15
12
 
13
+ type CardPropsWithItem = BaseCardProps & {
14
+ /** List of items rendered within the card. Mutually exclusive with `href`. */
15
+ href?: never;
16
+ item: Partial<ObjectBrowserItem>;
17
+ };
18
+
19
+ type CardPropsWithoutItem = BaseCardProps & {
20
+ /** Optional URL to make the card clickable as a link. */
21
+ href?: string | undefined | null;
22
+ item?: never;
23
+ };
24
+
25
+ type CardProps = CardPropsWithItem | CardPropsWithoutItem;
26
+
16
27
  const DefaultImage = (props: any) => {
17
28
  const { src, item, imageField, alt, loading, responsive } = props;
18
29
  return (
@@ -35,7 +46,10 @@ const childrenWithProps = (children, extraProps) => {
35
46
  };
36
47
 
37
48
  const Card = (props: CardProps) => {
38
- const { className, href, openLinkInNewTab } = props;
49
+ const hasItem = !!props.item;
50
+ const item = hasItem ? props.item : undefined;
51
+ const href = !hasItem ? props.href : undefined;
52
+ const { className, openLinkInNewTab } = props;
39
53
 
40
54
  const a11yLabelId = React.useId();
41
55
  const linkRef = React.useRef<HTMLAnchorElement>(null);
@@ -48,12 +62,14 @@ const Card = (props: CardProps) => {
48
62
  }
49
63
  };
50
64
 
65
+ const isInteractive = !!props.href || !!props.item;
66
+
51
67
  const onClick: React.MouseEventHandler<HTMLDivElement> = () => {
52
- if (href) triggerNavigation();
68
+ if (isInteractive) triggerNavigation();
53
69
  };
54
70
 
55
71
  const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
56
- if (!href) return;
72
+ if (!isInteractive) return;
57
73
  if (e.key === 'Enter' || e.key === ' ') {
58
74
  e.preventDefault();
59
75
  triggerNavigation();
@@ -63,16 +79,17 @@ const Card = (props: CardProps) => {
63
79
  return (
64
80
  <div
65
81
  className={cx('card', className)}
66
- onClick={onClick}
67
- onKeyDown={onKeyDown}
68
- role={href ? 'link' : undefined}
69
- tabIndex={href ? 0 : undefined}
82
+ onClick={isInteractive ? onClick : undefined}
83
+ onKeyDown={isInteractive ? onKeyDown : undefined}
84
+ role={isInteractive ? 'link' : undefined}
85
+ tabIndex={isInteractive ? 0 : undefined}
70
86
  >
71
87
  {/* @ts-expect-error since this has no children, should fail */}
72
88
  <ConditionalLink
73
89
  aria-labelledby={a11yLabelId}
74
- condition={!!href}
90
+ condition={isInteractive}
75
91
  href={href}
92
+ item={item}
76
93
  openLinkInNewTab={openLinkInNewTab}
77
94
  ref={linkRef}
78
95
  />
@@ -98,7 +98,7 @@ export const teaserBlock = {
98
98
  'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea.',
99
99
  getRemoteUrl: null,
100
100
  hasPreviewImage: true,
101
- head_title: 'Head title',
101
+ head_title: 'Kicker',
102
102
  image_field: 'preview_image',
103
103
  image_scales: {
104
104
  preview_image: [
@@ -412,7 +412,7 @@ export const gridBlock = {
412
412
  '@type': 'teaser',
413
413
  description:
414
414
  'Lorem ipsum dolor sit amet adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.',
415
- head_title: 'Head title ',
415
+ head_title: 'Kicker',
416
416
  href: [
417
417
  {
418
418
  '@id': '.',
@@ -425,7 +425,7 @@ export const gridBlock = {
425
425
  title: 'Block: Teaser',
426
426
  getRemoteUrl: null,
427
427
  hasPreviewImage: true,
428
- head_title: 'Head Title',
428
+ head_title: 'Kicker',
429
429
  image_field: 'preview_image',
430
430
  image_scales: {
431
431
  preview_image: [
@@ -502,7 +502,7 @@ export const gridBlock = {
502
502
  '@type': 'teaser',
503
503
  description:
504
504
  'Lorem ipsum dolor sit amet adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.',
505
- head_title: 'Head title ',
505
+ head_title: 'Kicker',
506
506
  href: [
507
507
  {
508
508
  '@id': '.',
@@ -515,7 +515,7 @@ export const gridBlock = {
515
515
  Title: 'Block: Grid',
516
516
  getRemoteUrl: null,
517
517
  hasPreviewImage: true,
518
- head_title: 'Head Title',
518
+ head_title: 'Kicker',
519
519
  image_field: 'preview_image',
520
520
  image_scales: {
521
521
  preview_image: [
@@ -150,3 +150,8 @@
150
150
  #page-edit .block-editor-teaser.has--backgroundColor--grey {
151
151
  background-color: $lightgrey;
152
152
  }
153
+
154
+ .file-teaser .headline span:not(:last-child)::after {
155
+ margin: 0 10px;
156
+ content: '|';
157
+ }