Skip to content

Scroller mode and full width for teaser lists #2242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
de:
pageflow:
teaser_list_scroller:
feature_name: "Teaser List Scroller"
pageflow_scrolled:
editor:
aspect_ratios:
Expand All @@ -15,6 +18,11 @@ de:
content_elements:
externalLinkList:
attributes:
enableScroller:
label: "Layout-Verhalten"
values:
never: "In mehrere Zeilen umbrechen"
always: "Horizontal scrollen"
thumbnailAspectRatio:
label: "Thumbnail Seitenverhältnis"
inline_help: |-
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
en:
pageflow:
teaser_list_scroller:
feature_name: "Teaser List Scroller"
pageflow_scrolled:
editor:
aspect_ratios:
Expand All @@ -15,6 +18,11 @@ en:
content_elements:
externalLinkList:
attributes:
enableScroller:
label: "Layout behavior"
values:
never: "Wrap into multiple lines"
always: "Scroll horizontally"
thumbnailAspectRatio:
label: "Thumbnail aspect ratio"
inline_help: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,14 @@ de:
pageflow_scrolled:
editor:
content_element_categories:
links: DELETED
tilesAndLinks:
name: Verweise und Kacheln
content_elements:
externalLinkList:
attributes:
description: DELETED
open_in_new_tab: DELETED
title: DELETED
url: DELETED
description: Liste von Kacheln mit Bild und Beschriftung
confirm_delete_item: Soll der Teaser wirklich gelöscht werden?
confirm_delete: DELETED
outline: DELETED
items: Einträge
name: Teaser-Liste
tabs:
general: Teaser-Liste
edit_link: Teaser
inline_editing:
external_links: DELETED
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,14 @@ en:
pageflow_scrolled:
editor:
content_element_categories:
links: DELETED
tilesAndLinks:
name: Links and Tiles
content_elements:
externalLinkList:
attributes:
description: DELETED
open_in_new_tab: DELETED
title: DELETED
url: DELETED
description: A list of tiles with thumbnail and caption
confirm_delete_item: Are you sure you want to delete this teaser?
confirm_delete: DELETED
outline: DELETED
items: Items
name: Teasers
tabs:
general: Teasers
edit_link: Teaser
inline_editing:
external_links: DELETED
1 change: 1 addition & 0 deletions entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def configure(config)
c.features.register('content_element_margins')
c.features.register('backdrop_size')
c.features.register('section_paddings')
c.features.register('teaser_list_scroller')

c.additional_frontend_seed_data.register(
'frontendVersion',
Expand Down
44 changes: 44 additions & 0 deletions entry_types/scrolled/package/spec/frontend/Layout-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ describe('Layout', () => {
}
});

frontend.contentElementTypes.register('probeWithCustomMarginFunction', {
customMargin({configuration}) { return configuration.useCustomMargin; },

component: function Probe({contentElementId}) {
return (
<div>{contentElementId} </div>
);
}
});

frontend.contentElementTypes.register('probeWithCustomMarginProp', {
customMargin: true,

Expand Down Expand Up @@ -144,6 +154,23 @@ describe('Layout', () => {
expect(container.textContent).toEqual('[inline normal 1 inline custom 2 inline normal 3 ]');
});

it('supports function for custom margin option', () => {
const items = [
{id: 1, type: 'probe', position: 'inline'},
{id: 2, type: 'probeWithCustomMarginFunction', position: 'inline', props: {useCustomMargin: true}},
{id: 3, type: 'probeWithCustomMarginFunction', position: 'inline', props: {useCustomMargin: false}}
];
const {container} = renderInEntry(
<Layout sectionProps={{layout: 'left'}} items={items}>
{(children, {position, customMargin}) =>
<div>{position} {customMargin ? 'custom' : 'normal'} {children}</div>
}
</Layout>
);

expect(container.textContent).toEqual('[inline normal 1 inline custom 2 inline normal 3 ]');
});

it('places side elements with and without custom margin in separate groups', () => {
const items = [
{id: 1, type: 'probe', position: 'side'},
Expand Down Expand Up @@ -699,6 +726,23 @@ describe('Layout', () => {
expect(container.textContent).toEqual('normal 1 custom 2 normal 3 ');
});

it('supports function for custom margin option', () => {
const items = [
{id: 1, type: 'probe', position: 'inline'},
{id: 2, type: 'probeWithCustomMarginFunction', position: 'inline', props: {useCustomMargin: true}},
{id: 3, type: 'probeWithCustomMarginFunction', position: 'inline', props: {useCustomMargin: false}}
];
const {container} = renderInEntry(
<Layout sectionProps={{layout: 'center'}} items={items}>
{(children, {customMargin}) =>
<div>{customMargin ? 'custom' : 'normal'} {children}</div>
}
</Layout>
);

expect(container.textContent).toEqual('normal 1 custom 2 normal 3 ');
});

describe('customMargin prop passed to content element', () => {
it('is true if rendered inline with custom margin', () => {
const items = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {editor} from 'pageflow-scrolled/editor';
import {features} from 'pageflow/frontend';
import {SelectInputView, SliderInputView, SeparatorView, CheckBoxInputView} from 'pageflow/ui';

import {SidebarRouter} from './SidebarRouter';
Expand All @@ -21,7 +22,7 @@ editor.contentElementTypes.register('externalLinkList', {
pictogram,
category: 'tilesAndLinks',
supportedPositions: ['inline', 'standAlone'],
supportedWidthRange: ['m', 'xl'],
supportedWidthRange: ['m', 'full'],

defaultConfig: {
thumbnailAspectRatio: 'square'
Expand Down Expand Up @@ -56,6 +57,13 @@ editor.contentElementTypes.register('externalLinkList', {
this.view(SeparatorView);
this.group('ContentElementPosition', {entry});
this.view(SeparatorView);

if (features.isEnabled('teaser_list_scroller')) {
this.input('enableScroller', SelectInputView, {
values: ['never', 'always']
});
}

this.input('linkWidth', SliderInputView, {
displayText: value => [
'XS', 'S', 'M', 'L', 'XL', 'XXL'
Expand All @@ -68,8 +76,16 @@ editor.contentElementTypes.register('externalLinkList', {
});
this.input('linkAlignment', SelectInputView, {
values: ['spaceEvenly', 'left', 'right', 'center'],
visibleBinding: 'textPosition',
visible: textPosition => textPosition !== 'right'
visibleBinding: ['textPosition', 'enableScroller'],
visible: ([textPosition, enableScroller]) =>
textPosition !== 'right' && enableScroller !== 'always',
});
this.input('linkAlignment', SelectInputView, {
values: ['left'],
disabled: true,
visibleBinding: ['textPosition', 'enableScroller'],
visible: ([textPosition, enableScroller]) =>
textPosition !== 'right' && enableScroller === 'always',
});
this.input('thumbnailSize', SelectInputView, {
values: ['small', 'medium', 'large'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function ExternalLink({id, configuration, ...props}) {
<Link isEnabled={!configuration.displayButtons}
isEditable={isEditable}
actionButtonVisible={props.selected}
actionButtonPortal={true}
href={href}
openInNewTab={openInNewTab}
onChange={handleLinkChange}>
Expand Down Expand Up @@ -178,8 +179,7 @@ function Link({isEnabled, isEditable, ...props}) {
if ((isEnabled && props.href) || isEditable) {
return (
<EditableLink {...props}
actionButtonVisible={props.actionButtonVisible}
linkPreviewPosition="above" />
actionButtonVisible={props.actionButtonVisible} />
);
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
position: relative;
margin: 0;
padding: 0;
transition: transform 0.3s;
}

.item > a {
Expand All @@ -35,10 +34,11 @@
}

.textPosition-below .cardWrapper,
.textPosition-right .cardWrapper{
.textPosition-right .cardWrapper {
height: 100%;
display: flex;
flex-direction: column;
transition: transform 0.3s;
}

.card {
Expand Down Expand Up @@ -70,12 +70,16 @@
composes: textPosition-none;
}

.link.textPosition-below:not(.outlined):hover {
transform: scale(var(--theme-external-links-card-hover-scale, 1.05));
.link.textPosition-below .cardWrapper {
--hover-scale: var(--theme-external-links-card-hover-scale, 1.05);
}

.link.textPosition-right .cardWrapper {
--hover-scale: var(--theme-external-links-card-hover-scale, 1.02);
}

.link.textPosition-right:not(.outlined):hover {
transform: scale(var(--theme-external-links-card-hover-scale, 1.02));
.link:not(.outlined):hover .cardWrapper {
transform: scale(calc(1 + (var(--hover-scale) - 1) * var(--hover-scale-adjustment, 1)));
}

.link .card:hover .title {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import React, {useState} from 'react';
import classNames from 'classnames';
import {
LinkTooltipProvider,
useContentElementEditorCommandSubscription,
useContentElementEditorState,
useContentElementLifecycle,
useDarkBackground,
contentElementWidthName
useTheme,
contentElementWidthName,
contentElementWidths
} from 'pageflow-scrolled/frontend';
import {ExternalLink} from './ExternalLink';
import {Scroller} from './Scroller';
import styles from './ExternalLinkList.module.css';

import textPositionBelowStyles from './textPositons/below.module.css';
Expand All @@ -26,6 +30,7 @@ export function ExternalLinkList(props) {
const linkList = props.configuration.links || [];
const {shouldLoad} = useContentElementLifecycle();
const darkBackground = useDarkBackground();
const theme = useTheme();

const {setTransientState, isSelected} = useContentElementEditorState();
const [selectedItemId, setSelectedItemId] = useState();
Expand Down Expand Up @@ -72,42 +77,63 @@ export function ExternalLinkList(props) {
textPositionRightStyles :
textPositionBelowStyles;

const scrollerEnabled = props.configuration.enableScroller === 'always';
const fullWidth = props.contentElementWidth === contentElementWidths.full;
const linkAlignment = scrollerEnabled ? 'left' : props.configuration.linkAlignment;

return (
<div className={styles.container}>
<ul className={classNames(
styles.list,
styles[`textPosition-${textPosition}`],
<div className={classNames({[styles.contentMargin]: props.customMargin || fullWidth},
styles[`scrollButtons-${theme.options.teasersScrollButtons}`])}
onClick={handleListClick}>
<Scroller enabled={scrollerEnabled} measureKey={linkList.length}>
{({scrollerRef, handleScroll}) =>
<div className={classNames(styles.container,
{[styles.fullContainer]: fullWidth},
textPositionStyles.container)}>
<LinkTooltipProvider align="center">
<ul ref={scrollerRef}
className={classNames(
styles.list,
styles[`textPosition-${textPosition}`],
styles[`layout-${layout}`],
{[styles.full]: fullWidth},
{[styles.scroller]: scrollerEnabled},

props.configuration.variant &&
`scope-externalLinkList-${props.configuration.variant}`,
props.configuration.variant &&
`scope-externalLinkList-${props.configuration.variant}`,

textPositionStyles.list,
textPositionStyles[`layout-${layout}`],
textPositionStyles[`width-${contentElementWidthName(props.configuration.width)}`],
textPositionStyles[`linkWidth-${linkWidth}`],
textPositionStyles[`linkAlignment-${props.configuration.linkAlignment}`],
textPositionStyles[`textPosition-${textPosition}`]
)}
style={{'--overlay-opacity': (props.configuration.overlayOpacity || 70) / 100,
'--thumbnail-aspect-ratio': `var(--theme-aspect-ratio-${props.configuration.thumbnailAspectRatio || 'wide'})`}}
onClick={handleListClick}>
{linkList.map((link, index) =>
<ExternalLink {...link}
key={link.id}
configuration={props.configuration}
thumbnailAspectRatio={props.configuration.thumbnailAspectRatio}
thumbnailSize={props.configuration.thumbnailSize || 'small'}
thumbnailFit={props.configuration.thumbnailFit || 'cover'}
textPosition={props.configuration.textPosition || 'below'}
textSize={props.configuration.textSize || 'small'}
darkBackground={darkBackground}
loadImages={shouldLoad}
outlined={isSelected}
highlighted={highlightedIndex === index}
selected={link.id === selectedItemId && isSelected}
onClick={event => handleItemClick(event, link.id)} />
)}
</ul>
textPositionStyles.list,
textPositionStyles[`layout-${layout}`],
textPositionStyles[`width-${contentElementWidthName(props.contentElementWidth)}`],
textPositionStyles[`linkWidth-${fullWidth ? 'full-' : ''}${linkWidth}`],
textPositionStyles[`linkAlignment-${linkAlignment}`],
textPositionStyles[`textPosition-${textPosition}`],
{[textPositionStyles.scroller]: scrollerEnabled}
)}
style={{'--overlay-opacity': (props.configuration.overlayOpacity || 70) / 100,
'--thumbnail-aspect-ratio': `var(--theme-aspect-ratio-${props.configuration.thumbnailAspectRatio || 'wide'})`}}
onScroll={handleScroll}>
{linkList.map((link, index) =>
<ExternalLink {...link}
key={link.id}
configuration={props.configuration}
thumbnailAspectRatio={props.configuration.thumbnailAspectRatio}
thumbnailSize={props.configuration.thumbnailSize || 'small'}
thumbnailFit={props.configuration.thumbnailFit || 'cover'}
textPosition={props.configuration.textPosition || 'below'}
textSize={props.configuration.textSize || 'small'}
darkBackground={darkBackground}
loadImages={shouldLoad}
outlined={isSelected}
highlighted={highlightedIndex === index}
selected={link.id === selectedItemId && isSelected}
onClick={event => handleItemClick(event, link.id)} />
)}
</ul>
</LinkTooltipProvider>
</div>
}
</Scroller>
</div>
);
}
Loading
Loading