diff --git a/app/models/pageflow/customized_theme.rb b/app/models/pageflow/customized_theme.rb
index 5f081da23f..afc6ddf427 100644
--- a/app/models/pageflow/customized_theme.rb
+++ b/app/models/pageflow/customized_theme.rb
@@ -1,9 +1,9 @@
module Pageflow
# @api private
class CustomizedTheme < SimpleDelegator
- def initialize(theme, overrides, files)
+ def initialize(theme, transformed_options, overrides, files)
super(theme)
- @options = __getobj__.options.deep_merge(overrides || {})
+ @options = transformed_options.deep_merge(overrides || {})
@files = files
end
@@ -24,6 +24,7 @@ def self.build(entry:, theme:, theme_customization:)
config = Pageflow.config_for(entry)
new(theme,
+ config.themes.apply_default_options(theme.options),
config.transform_theme_customization_overrides.call(
theme_customization.overrides,
entry:,
diff --git a/entry_types/scrolled/config/locales/de.yml b/entry_types/scrolled/config/locales/de.yml
index ca51467025..7550431416 100644
--- a/entry_types/scrolled/config/locales/de.yml
+++ b/entry_types/scrolled/config/locales/de.yml
@@ -1309,6 +1309,28 @@ de:
add: Neues Element
unset: Nicht mehr als Hintergrund verwenden
scales:
+ sectionPaddingTop:
+ none: "-"
+ xxxs: XXXS
+ xxs: XXS
+ xs: XS
+ sm: S
+ md: M
+ lg: L
+ xl: XL
+ xxl: XXL
+ xxxl: XXXL
+ sectionPaddingBottom:
+ none: "-"
+ xxxs: XXXS
+ xxs: XXS
+ xs: XS
+ sm: S
+ md: M
+ lg: L
+ xl: XL
+ xxl: XXL
+ xxxl: XXXL
contentElementBoxBorderRadius:
none: Keine
content_element_text_inline_file_rights_attributes:
@@ -1672,6 +1694,8 @@ de:
cancel: Abbrechen
default_padding: Standard
drag_content_element: Ziehen, um Element zu verschieben
+ padding_suppressed_before_full_width: Abstand unterdrückt vor vollbreitem Element
+ padding_suppressed_after_full_width: Abstand unterdrückt nach vollbreitem Element
edit_section_transition_after: Übergangseffekt bearbeiten
edit_section_transition_before: Übergangseffekt bearbeiten
flip_card: Karte wenden
diff --git a/entry_types/scrolled/config/locales/en.yml b/entry_types/scrolled/config/locales/en.yml
index 34d70add8a..f3c219265c 100644
--- a/entry_types/scrolled/config/locales/en.yml
+++ b/entry_types/scrolled/config/locales/en.yml
@@ -1293,6 +1293,28 @@ en:
add: New element
unset: No longer use as backdrop
scales:
+ sectionPaddingTop:
+ none: "-"
+ xxxs: XXXS
+ xxs: XXS
+ xs: XS
+ sm: S
+ md: M
+ lg: L
+ xl: XL
+ xxl: XXL
+ xxxl: XXXL
+ sectionPaddingBottom:
+ none: "-"
+ xxxs: XXXS
+ xxs: XXS
+ xs: XS
+ sm: S
+ md: M
+ lg: L
+ xl: XL
+ xxl: XXL
+ xxxl: XXXL
contentElementBoxBorderRadius:
none: None
content_element_text_inline_file_rights_attributes:
@@ -1502,6 +1524,8 @@ en:
cancel: Cancel
default_padding: Default
drag_content_element: Drag to move element
+ padding_suppressed_before_full_width: Padding suppressed before full width element
+ padding_suppressed_after_full_width: Padding suppressed after full width element
edit_section_transition_after: Edit section transition
edit_section_transition_before: Edit section transition
flip_card: Flip card
diff --git a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
index 4a7b89562e..8ab10f3406 100644
--- a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
+++ b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
@@ -7,6 +7,46 @@ def configure(config)
end
config.for_entry_type(PageflowScrolled.entry_type) do |c|
+ padding_scale = {
+ 'none' => '0px',
+ 'xxxs' => '1.375em',
+ 'xxs' => '3em',
+ 'xs' => '4.375em',
+ 'sm' => '6em',
+ 'md' => 'max(7em, 10svh)',
+ 'lg' => 'max(8em, 10svh)',
+ 'xl' => 'max(9em, 15svh)',
+ 'xxl' => 'max(10em, 20svh)',
+ 'xxxl' => 'max(11em, 30svh)'
+ }
+
+ c.themes.register_default_options(
+ ThemeOptionsDefaultScale.new(
+ prefix: 'section_padding_top',
+ values: padding_scale
+ )
+ )
+
+ c.themes.register_default_options(
+ ThemeOptionsDefaultScale.new(
+ prefix: 'section_padding_bottom',
+ values: padding_scale
+ )
+ )
+
+ c.themes.register_default_options(
+ properties: {
+ root: {
+ 'section_default_padding_top' => '1.375em',
+ 'section_default_padding_bottom' => '4.375em'
+ },
+ cards_appearance_section: {
+ 'section_default_padding_top' => '3em',
+ 'section_default_padding_bottom' => '6em'
+ }
+ }
+ )
+
c.file_types.register(Pageflow::BuiltInFileType.image)
c.file_types.register(Pageflow::BuiltInFileType.video)
c.file_types.register(Pageflow::BuiltInFileType.audio)
diff --git a/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb b/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb
new file mode 100644
index 0000000000..0b6de884d7
--- /dev/null
+++ b/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb
@@ -0,0 +1,35 @@
+module PageflowScrolled
+ # Callable that ensures a scale with the given prefix is defined
+ # in theme options. If the theme already defines any properties
+ # with the prefix, the defaults are not added.
+ #
+ # @api private
+ class ThemeOptionsDefaultScale
+ def initialize(prefix:, values:)
+ @prefix = prefix
+ @values = values
+ end
+
+ def call(options)
+ return options if scale_defined?(options)
+
+ options.deep_merge(
+ properties: {
+ root: prefixed_values
+ }
+ )
+ end
+
+ private
+
+ def scale_defined?(options)
+ options.dig(:properties, :root)&.keys&.any? do |key|
+ key.to_s.start_with?("#{@prefix}-")
+ end
+ end
+
+ def prefixed_values
+ @values.transform_keys { |key| "#{@prefix}-#{key}" }
+ end
+ end
+end
diff --git a/entry_types/scrolled/package/spec/editor/views/EditSectionPaddingsView-spec.js b/entry_types/scrolled/package/spec/editor/views/EditSectionPaddingsView-spec.js
index 764b721488..99fcf4d4e0 100644
--- a/entry_types/scrolled/package/spec/editor/views/EditSectionPaddingsView-spec.js
+++ b/entry_types/scrolled/package/spec/editor/views/EditSectionPaddingsView-spec.js
@@ -760,4 +760,103 @@ describe('EditSectionPaddingsView', () => {
expect(listener).toHaveBeenCalledWith(entry.sections.get(1), {align: 'nearEnd', ifNeeded: true});
});
});
+
+ describe('slider default value', () => {
+ useFakeTranslations({
+ 'pageflow_scrolled.editor.edit_section_paddings.tabs.sectionPaddings': 'Landscape',
+ 'pageflow_scrolled.editor.section_padding_visualization.top_padding': 'TopPadding',
+ 'pageflow_scrolled.editor.section_padding_visualization.bottom_padding': 'Bottom'
+ });
+
+ it('displays default value text when section has no paddingTop set', () => {
+ const entry = createEntry({
+ sections: [{id: 1, configuration: {backdropType: 'color'}}],
+ themeOptions: {
+ properties: {
+ root: {
+ sectionDefaultPaddingTop: '3em',
+ 'sectionPaddingTop-none': '0',
+ 'sectionPaddingTop-sm': '1.375em',
+ 'sectionPaddingTop-lg': '3em',
+ 'sectionPaddingBottom-none': '0',
+ 'sectionPaddingBottom-sm': '1.375em',
+ 'sectionPaddingBottom-lg': '3em'
+ }
+ }
+ },
+ themeTranslations: {
+ scales: {
+ sectionPaddingTop: {
+ none: 'None',
+ sm: 'Small',
+ lg: 'Large'
+ },
+ sectionPaddingBottom: {
+ none: 'None',
+ sm: 'Small',
+ lg: 'Large'
+ }
+ }
+ }
+ });
+
+ const view = new EditSectionPaddingsView({
+ model: entry.sections.get(1),
+ entry
+ });
+
+ renderBackboneView(view);
+
+ // Find the first slider's value display element
+ const sliderValueText = view.$el.find('.slider_input .value').eq(0).text();
+ expect(sliderValueText).toEqual('Large');
+ });
+
+ it('prefers appearance-specific default over root default', () => {
+ const entry = createEntry({
+ sections: [{id: 1, configuration: {backdropType: 'color', appearance: 'cards'}}],
+ themeOptions: {
+ properties: {
+ root: {
+ sectionDefaultPaddingTop: '1.375em',
+ 'sectionPaddingTop-none': '0',
+ 'sectionPaddingTop-sm': '1.375em',
+ 'sectionPaddingTop-lg': '3em',
+ 'sectionPaddingBottom-none': '0',
+ 'sectionPaddingBottom-sm': '1.375em',
+ 'sectionPaddingBottom-lg': '3em'
+ },
+ cardsAppearanceSection: {
+ sectionDefaultPaddingTop: '3em'
+ }
+ }
+ },
+ themeTranslations: {
+ scales: {
+ sectionPaddingTop: {
+ none: 'None',
+ sm: 'Small',
+ lg: 'Large'
+ },
+ sectionPaddingBottom: {
+ none: 'None',
+ sm: 'Small',
+ lg: 'Large'
+ }
+ }
+ }
+ });
+
+ const view = new EditSectionPaddingsView({
+ model: entry.sections.get(1),
+ entry
+ });
+
+ renderBackboneView(view);
+
+ // Should display 'Large' (3em from cardsAppearanceSection) not 'Small' (1.375em from root)
+ const sliderValueText = view.$el.find('.slider_input .value').eq(0).text();
+ expect(sliderValueText).toEqual('Large');
+ });
+ });
});
diff --git a/entry_types/scrolled/package/spec/frontend/Layout-spec.js b/entry_types/scrolled/package/spec/frontend/Layout-spec.js
index 831b170bd8..83e51b666d 100644
--- a/entry_types/scrolled/package/spec/frontend/Layout-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/Layout-spec.js
@@ -814,6 +814,105 @@ describe('Layout', () => {
expect(container.textContent).toEqual('( 1 || 2 || 3 )');
});
});
+
+ describe('atSectionStart and atSectionEnd props', () => {
+ beforeAll(() => {
+ TwoColumn.GroupComponent = 'div';
+ });
+
+ const SectionBoundaryBox = function SectionBoundaryBox({atSectionStart, atSectionEnd, children}) {
+ return (
+
{atSectionStart ? '[' : ''}{children}{atSectionEnd ? ']' : ''}
+ );
+ };
+
+ it('marks first box with atSectionStart in two column variant', () => {
+ const items = [
+ {id: 1, type: 'probe', position: 'inline'},
+ {id: 2, type: 'probe', position: 'inline'},
+ ];
+ const {container} = renderInEntry(
+
+ {(children, boxProps) => {children}}
+
+ );
+
+ expect(container.textContent).toEqual('[1 2 ]');
+ });
+
+ it('marks first and last boxes in multiple groups', () => {
+ const items = [
+ {id: 1, type: 'probe', position: 'inline'},
+ {id: 2, type: 'probe', position: 'sticky'},
+ {id: 3, type: 'probe', position: 'inline'},
+ ];
+ const {container} = renderInEntry(
+
+ {(children, boxProps) => {children}}
+
+ );
+
+ expect(container.textContent).toEqual('[1 2 3 ]');
+ });
+
+ it('marks first inline box with atSectionStart when first element is sticky', () => {
+ const items = [
+ {id: 1, type: 'probe', position: 'sticky'},
+ {id: 2, type: 'probe', position: 'inline'},
+ {id: 3, type: 'probe', position: 'inline'},
+ ];
+ const {container} = renderInEntry(
+
+ {(children, boxProps) => {children}}
+
+ );
+
+ expect(container.textContent).toEqual('1 [2 3 ]');
+ });
+
+ it('marks first and last boxes in center variant', () => {
+ const items = [
+ {id: 1, type: 'probe', position: 'inline'},
+ {id: 2, type: 'probe', position: 'inline'},
+ {id: 3, type: 'probe', position: 'inline'},
+ ];
+ const {container} = renderInEntry(
+
+ {(children, boxProps) => {children}}
+
+ );
+
+ expect(container.textContent).toEqual('[1 2 3 ]');
+ });
+
+ it('does not mark first box with atSectionStart when isContentPadded in two column variant', () => {
+ const items = [
+ {id: 1, type: 'probe', position: 'inline'},
+ {id: 2, type: 'probe', position: 'inline'},
+ ];
+ const {container} = renderInEntry(
+
+ {(children, boxProps) => {children}}
+
+ );
+
+ expect(container.textContent).toEqual('1 2 ]');
+ });
+
+ it('does not mark first box with atSectionStart when isContentPadded in center variant', () => {
+ const items = [
+ {id: 1, type: 'probe', position: 'inline'},
+ {id: 2, type: 'probe', position: 'inline'},
+ ];
+ const {container} = renderInEntry(
+
+ {(children, boxProps) => {children}}
+
+ );
+
+ expect(container.textContent).toEqual('1 2 ]');
+ });
+ });
});
describe('floating items in centered variant', () => {
diff --git a/entry_types/scrolled/package/spec/frontend/features/contentElementMargin-spec.js b/entry_types/scrolled/package/spec/frontend/features/contentElementMargin-spec.js
index d973b51838..2254fa316e 100644
--- a/entry_types/scrolled/package/spec/frontend/features/contentElementMargin-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/features/contentElementMargin-spec.js
@@ -1,6 +1,8 @@
+import React from 'react';
+
import {renderEntry, usePageObjects} from 'support/pageObjects';
-import {contentElementWidths as widths} from 'pageflow-scrolled/frontend';
+import {contentElementWidths as widths, frontend} from 'pageflow-scrolled/frontend';
describe('content element margin', () => {
usePageObjects();
@@ -30,4 +32,54 @@ describe('content element margin', () => {
expect(getContentElementByTestId(1).hasMargin()).toBe(false);
});
+
+ it('does not apply top margin to first content element in section', () => {
+ const {getContentElementByTestId} = renderEntry({
+ seed: {
+ sections: [{id: 5}],
+ contentElements: [
+ {sectionId: 5, typeName: 'withTestId', configuration: {testId: 1}},
+ {sectionId: 5, typeName: 'withTestId', configuration: {testId: 2}}
+ ]
+ }
+ });
+
+ expect(getContentElementByTestId(1).hasTopMargin()).toBe(false);
+ expect(getContentElementByTestId(2).hasTopMargin()).toBe(true);
+ });
+
+ it('still applies top margin to first content element in cards appearance', () => {
+ const {getContentElementByTestId} = renderEntry({
+ seed: {
+ sections: [{id: 5, configuration: {appearance: 'cards'}}],
+ contentElements: [
+ {sectionId: 5, typeName: 'withTestId', configuration: {testId: 1}},
+ {sectionId: 5, typeName: 'withTestId', configuration: {testId: 2}}
+ ]
+ }
+ });
+
+ expect(getContentElementByTestId(1).hasTopMargin()).toBe(true);
+ expect(getContentElementByTestId(2).hasTopMargin()).toBe(true);
+ });
+
+ it('supports defaultMarginTop option in content element registration', () => {
+ frontend.contentElementTypes.register('withDefaultMargin', {
+ component: function WithDefaultMargin({configuration}) {
+ return ;
+ },
+ defaultMarginTop: '1.375rem'
+ });
+
+ const {getContentElementByTestId} = renderEntry({
+ seed: {
+ sections: [{id: 5, configuration: {appearance: 'cards'}}],
+ contentElements: [
+ {sectionId: 5, typeName: 'withDefaultMargin', configuration: {testId: 1}}
+ ]
+ }
+ });
+
+ expect(getContentElementByTestId(1).getMarginTop()).toBe('1.375rem');
+ });
});
diff --git a/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js b/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js
index 3a183fbc1a..312bf90929 100644
--- a/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js
@@ -103,4 +103,75 @@ describe('PaddingIndicator', () => {
expect(section.getPaddingIndicator('top')).toHaveTextContent('Expose motif area');
});
+
+ it('displays suppressed text for top padding when section starts with full width element', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{
+ id: 1,
+ permaId: 10
+ }],
+ contentElements: [{sectionId: 1, configuration: {width: 3}}]
+ }
+ });
+
+ const section = getSectionByPermaId(10);
+ section.select();
+
+ expect(section.getPaddingIndicator('top')).toHaveTextContent('Padding suppressed before full width element');
+ });
+
+ it('displays suppressed text for bottom padding when section ends with full width element', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{
+ id: 1,
+ permaId: 10
+ }],
+ contentElements: [{sectionId: 1, configuration: {width: 3}}]
+ }
+ });
+
+ const section = getSectionByPermaId(10);
+ section.select();
+
+ expect(section.getPaddingIndicator('bottom')).toHaveTextContent('Padding suppressed after full width element');
+ });
+
+ it('applies none class to padding indicator when padding is suppressed', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{
+ id: 1,
+ permaId: 10
+ }],
+ contentElements: [{sectionId: 1, configuration: {width: 3}}]
+ }
+ });
+
+ const section = getSectionByPermaId(10);
+ section.select();
+
+ expect(section.getPaddingIndicator('top').className).toContain('none');
+ });
+
+ it('applies none class to padding indicator when padding value is none', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{
+ id: 1,
+ permaId: 10,
+ configuration: {
+ paddingTop: 'none'
+ }
+ }],
+ contentElements: [{sectionId: 1}]
+ }
+ });
+
+ const section = getSectionByPermaId(10);
+ section.select();
+
+ expect(section.getPaddingIndicator('top').className).toContain('none');
+ });
});
diff --git a/entry_types/scrolled/package/spec/frontend/features/sectionAppearanceScopeClass-spec.js b/entry_types/scrolled/package/spec/frontend/features/sectionAppearanceScopeClass-spec.js
new file mode 100644
index 0000000000..b96917d3b1
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/features/sectionAppearanceScopeClass-spec.js
@@ -0,0 +1,37 @@
+import {renderEntry} from 'support/pageObjects';
+import '@testing-library/jest-dom/extend-expect';
+
+describe('section appearance scope class', () => {
+ it('applies scope class for default shadow appearance', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6}],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).el).toHaveClass('scope-shadowAppearanceSection');
+ });
+
+ it('applies scope class for cards appearance', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6, configuration: {appearance: 'cards'}}],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).el).toHaveClass('scope-cardsAppearanceSection');
+ });
+
+ it('applies scope class for transparent appearance', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6, configuration: {appearance: 'transparent'}}],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).el).toHaveClass('scope-transparentAppearanceSection');
+ });
+});
diff --git a/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js b/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js
index 1d37490b20..8acf9c8102 100644
--- a/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js
@@ -1,14 +1,33 @@
import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects';
+import {act} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
+import {features} from 'pageflow/frontend';
import {usePortraitOrientation} from 'frontend/usePortraitOrientation';
+import {useMotifAreaState} from 'frontend/v1/useMotifAreaState';
jest.mock('frontend/usePortraitOrientation');
+jest.mock('frontend/v1/useMotifAreaState');
describe('section padding', () => {
+ beforeEach(() => {
+ features.enable('frontend', ['section_paddings']);
+ });
+
useInlineEditingPageObjects();
- it('adds padding to bottom of section by default', () => {
+ it('does not suppress top padding by default', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6}],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).hasSuppressedTopPadding()).toBe(false);
+ });
+
+ it('does not suppress bottom padding by default', () => {
const {getSectionByPermaId} = renderEntry({
seed: {
sections: [{id: 5, permaId: 6}],
@@ -16,10 +35,10 @@ describe('section padding', () => {
}
});
- expect(getSectionByPermaId(6).hasBottomPadding()).toBe(true);
+ expect(getSectionByPermaId(6).hasSuppressedBottomPadding()).toBe(false);
});
- it('does not add padding to bottom of section if last content element is full width', () => {
+ it('suppresses top padding if first content element is full width', () => {
const {getSectionByPermaId} = renderEntry({
seed: {
sections: [{id: 5, permaId: 6}],
@@ -27,38 +46,107 @@ describe('section padding', () => {
}
});
- expect(getSectionByPermaId(6).hasBottomPadding()).toBe(false);
+ expect(getSectionByPermaId(6).hasSuppressedTopPadding()).toBe(true);
+ });
+
+ it('suppresses top padding if motif area is content padded', () => {
+ useMotifAreaState.mockContentPadded();
+
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6}],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).hasSuppressedTopPadding()).toBe(true);
});
- it('adds padding below full width element if section is selected', () => {
+ it('does not suppress first box top margin if motif area becomes content padded', () => {
const {getSectionByPermaId} = renderEntry({
seed: {
sections: [{id: 5, permaId: 6}],
- contentElements: [{sectionId: 5, configuration: {position: 'full'}}]
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).hasFirstBoxSuppressedTopMargin()).toBe(true);
+
+ act(() => useMotifAreaState.mockContentPadded());
+
+ expect(getSectionByPermaId(6).hasFirstBoxSuppressedTopMargin()).toBe(false);
+ });
+
+ it('suppresses bottom padding if last content element is full width', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6}],
+ contentElements: [{sectionId: 5, configuration: {width: 3}}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).hasSuppressedBottomPadding()).toBe(true);
+ });
+
+ it('forces padding below full width element if section is selected', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6}],
+ contentElements: [{sectionId: 5, configuration: {width: 3}}]
}
});
const section = getSectionByPermaId(6);
section.select();
- expect(section.hasBottomPadding()).toBe(true);
+ expect(section.hasForcedPadding()).toBe(true);
});
- it('adds padding below full width element if element is selected', () => {
+ it('forces padding below full width element if element is selected', () => {
const {getSectionByPermaId, getContentElementByTestId} = renderEntry({
seed: {
sections: [{id: 5, permaId: 6}],
contentElements: [{
sectionId: 5,
typeName: 'withTestId',
- configuration: {testId: 10, position: 'full'}
+ configuration: {testId: 10, width: 3}
}]
}
});
getContentElementByTestId(10).select();
- expect(getSectionByPermaId(6).hasBottomPadding()).toBe(true);
+ expect(getSectionByPermaId(6).hasForcedPadding()).toBe(true);
+ });
+
+ it('does not force padding if padding is selected', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6}],
+ contentElements: [{sectionId: 5, configuration: {width: 3}}]
+ }
+ });
+
+ const section = getSectionByPermaId(6);
+ section.selectPadding('bottom');
+
+ expect(section.hasForcedPadding()).toBe(false);
+ });
+
+ it('does not set inline padding styles when no paddingTop/paddingBottom set', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 5, permaId: 6}],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).el).not.toHaveStyle({
+ '--foreground-padding-top': expect.anything(),
+ });
+ expect(getSectionByPermaId(6).el).not.toHaveStyle({
+ '--foreground-padding-bottom': expect.anything(),
+ });
});
it('supports setting custom foreground padding', () => {
@@ -87,6 +175,7 @@ describe('section padding', () => {
const {getSectionByPermaId} = renderEntry({
seed: {
+ imageFiles: [{id: 100, permaId: 100}],
sections: [{
id: 5,
permaId: 6,
@@ -94,7 +183,8 @@ describe('section padding', () => {
paddingTop: 'lg',
paddingBottom: 'md',
portraitPaddingTop: 'sm',
- portraitPaddingBottom: 'xs'
+ portraitPaddingBottom: 'xs',
+ backdrop: {image: 100, imageMobile: 100}
}
}],
contentElements: [{sectionId: 5}]
@@ -130,6 +220,61 @@ describe('section padding', () => {
});
});
+ it('ignores portrait paddings if customPortraitPaddings is false', () => {
+ usePortraitOrientation.mockReturnValue(true);
+
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ imageFiles: [{id: 100, permaId: 100}],
+ sections: [{
+ id: 5,
+ permaId: 6,
+ configuration: {
+ paddingTop: 'lg',
+ paddingBottom: 'md',
+ portraitPaddingTop: 'sm',
+ portraitPaddingBottom: 'xs',
+ customPortraitPaddings: false,
+ backdrop: {image: 100, imageMobile: 100}
+ }
+ }],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).el).toHaveStyle({
+ '--foreground-padding-top': 'var(--theme-section-padding-top-lg)',
+ '--foreground-padding-bottom': 'var(--theme-section-padding-bottom-md)',
+ });
+ });
+
+ it('ignores portrait paddings if no mobile backdrop is assigned', () => {
+ usePortraitOrientation.mockReturnValue(true);
+
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ imageFiles: [{id: 100, permaId: 100}],
+ sections: [{
+ id: 5,
+ permaId: 6,
+ configuration: {
+ paddingTop: 'lg',
+ paddingBottom: 'md',
+ portraitPaddingTop: 'sm',
+ portraitPaddingBottom: 'xs',
+ backdrop: {image: 100}
+ }
+ }],
+ contentElements: [{sectionId: 5}]
+ }
+ });
+
+ expect(getSectionByPermaId(6).el).toHaveStyle({
+ '--foreground-padding-top': 'var(--theme-section-padding-top-lg)',
+ '--foreground-padding-bottom': 'var(--theme-section-padding-bottom-md)',
+ });
+ });
+
it('centers content vertically by default', () => {
const {getSectionByPermaId} = renderEntry({
seed: {
diff --git a/entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js
new file mode 100644
index 0000000000..8266ef22ea
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import {render} from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+
+import CardBoxWrapper from 'frontend/foregroundBoxes/CardBoxWrapper';
+
+import boundaryMarginStyles from 'frontend/foregroundBoxes/BoxBoundaryMargin.module.css';
+
+describe('CardBoxWrapper', () => {
+ describe('at section boundaries', () => {
+ it('does not have noTopMargin class when not at section start', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).not.toHaveClass(boundaryMarginStyles.noTopMargin);
+ });
+
+ it('has noTopMargin class when at section start', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).toHaveClass(boundaryMarginStyles.noTopMargin);
+ });
+
+ it('does not have noBottomMargin class when not at section end', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).not.toHaveClass(boundaryMarginStyles.noBottomMargin);
+ });
+
+ it('has noBottomMargin class when at section end', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).toHaveClass(boundaryMarginStyles.noBottomMargin);
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/spec/frontend/foregroundBoxes/InvisibleBoxWrapper-spec.js b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/InvisibleBoxWrapper-spec.js
new file mode 100644
index 0000000000..fcadf7fb86
--- /dev/null
+++ b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/InvisibleBoxWrapper-spec.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import {render} from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+
+import {InvisibleBoxWrapper} from 'frontend/foregroundBoxes/InvisibleBoxWrapper';
+
+import boundaryMarginStyles from 'frontend/foregroundBoxes/BoxBoundaryMargin.module.css';
+
+describe('InvisibleBoxWrapper', () => {
+ describe('at section boundaries', () => {
+ it('does not have noTopMargin class when not at section start', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).not.toHaveClass(boundaryMarginStyles.noTopMargin);
+ });
+
+ it('has noTopMargin class when at section start', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).toHaveClass(boundaryMarginStyles.noTopMargin);
+ });
+
+ it('does not have noBottomMargin class when not at section end', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).not.toHaveClass(boundaryMarginStyles.noBottomMargin);
+ });
+
+ it('has noBottomMargin class when at section end', () => {
+ const {container} = render(
+
+ Content
+
+ );
+
+ expect(container.firstChild).toHaveClass(boundaryMarginStyles.noBottomMargin);
+ });
+ });
+});
diff --git a/entry_types/scrolled/package/spec/support/pageObjects.js b/entry_types/scrolled/package/spec/support/pageObjects.js
index af247e01a9..c228d9ef6b 100644
--- a/entry_types/scrolled/package/spec/support/pageObjects.js
+++ b/entry_types/scrolled/package/spec/support/pageObjects.js
@@ -10,6 +10,7 @@ import contentElementScrollSpaceStyles from 'frontend/ContentElementScrollSpace.
import fitViewportStyles from 'frontend/FitViewport.module.css';
import centerLayoutStyles from 'frontend/layouts/Center.module.css';
import twoColumnLayoutStyles from 'frontend/layouts/TwoColumn.module.css';
+import boxBoundaryMarginStyles from 'frontend/foregroundBoxes/BoxBoundaryMargin.module.css';
import {StaticPreview} from 'frontend/useScrollPositionLifecycle';
import {loadInlineEditingComponents} from 'frontend/inlineEditing';
import {api} from 'frontend/api';
@@ -79,7 +80,9 @@ export function useInlineEditingPageObjects() {
'pageflow_scrolled.inline_editing.edit_section_padding_top': 'Edit top padding',
'pageflow_scrolled.inline_editing.edit_section_padding_bottom': 'Edit bottom padding',
'pageflow_scrolled.inline_editing.expose_motif_area': 'Expose motif area',
- 'pageflow_scrolled.inline_editing.default_padding': 'Default'
+ 'pageflow_scrolled.inline_editing.default_padding': 'Default',
+ 'pageflow_scrolled.inline_editing.padding_suppressed_before_full_width': 'Padding suppressed before full width element',
+ 'pageflow_scrolled.inline_editing.padding_suppressed_after_full_width': 'Padding suppressed after full width element'
});
usePageObjects();
@@ -196,8 +199,16 @@ function createSectionPageObject(el) {
fireEvent.mouseDown(getByTitle('Edit section transition after'));
},
- hasBottomPadding() {
- return foreground.classList.contains(foregroundStyles.paddingBottom);
+ hasSuppressedTopPadding() {
+ return foreground.classList.contains(foregroundStyles.suppressedPaddingTop);
+ },
+
+ hasSuppressedBottomPadding() {
+ return foreground.classList.contains(foregroundStyles.suppressedPaddingBottom);
+ },
+
+ hasForcedPadding() {
+ return foreground.classList.contains(foregroundStyles.forcePadding);
},
hasRemainingSpaceAbove() {
@@ -219,6 +230,16 @@ function createSectionPageObject(el) {
bottom: 'Edit bottom padding'
};
return getByLabelText(labels[position]);
+ },
+
+ selectPadding(position) {
+ fireEvent.mouseDown(selectionRect);
+ fireEvent.click(this.getPaddingIndicator(position));
+ },
+
+ hasFirstBoxSuppressedTopMargin() {
+ const firstBox = foreground.querySelector(`.${boxBoundaryMarginStyles.noTopMargin}`);
+ return !!firstBox;
}
}
}
@@ -250,6 +271,16 @@ function createContentElementPageObject(el) {
return !!el.closest(`.${contentElementMarginStyles.wrapper}`);
},
+ hasTopMargin() {
+ const wrapper = el.closest(`.${contentElementMarginStyles.wrapper}`);
+ return wrapper && !wrapper.classList.contains(contentElementMarginStyles.noTopMargin);
+ },
+
+ getMarginTop() {
+ const wrapper = el.closest(`.${contentElementMarginStyles.wrapper}`);
+ return wrapper && wrapper.style.marginTop;
+ },
+
hasScrollSpace() {
return !!el.closest(`.${contentElementScrollSpaceStyles.wrapper}`);
},
diff --git a/entry_types/scrolled/package/src/contentElements/heading/Heading.module.css b/entry_types/scrolled/package/src/contentElements/heading/Heading.module.css
index 6fc1996cb9..fc10654951 100644
--- a/entry_types/scrolled/package/src/contentElements/heading/Heading.module.css
+++ b/entry_types/scrolled/package/src/contentElements/heading/Heading.module.css
@@ -4,8 +4,7 @@
) from "pageflow-scrolled/values/colors.module.css";
.root {
- margin-top: 0.3em;
- margin-bottom: 0;
+ margin: 0;
padding-top: 0.45em;
}
diff --git a/entry_types/scrolled/package/src/contentElements/question/Question.module.css b/entry_types/scrolled/package/src/contentElements/question/Question.module.css
index 4095decb1a..57fbcca0e7 100644
--- a/entry_types/scrolled/package/src/contentElements/question/Question.module.css
+++ b/entry_types/scrolled/package/src/contentElements/question/Question.module.css
@@ -1,7 +1,6 @@
.details {
position: relative;
padding-left: 22px;
- margin: 1.375rem 0 0 0;
}
.details summary {
diff --git a/entry_types/scrolled/package/src/contentElements/question/frontend.js b/entry_types/scrolled/package/src/contentElements/question/frontend.js
index 69e1ca7069..b1feced1fd 100644
--- a/entry_types/scrolled/package/src/contentElements/question/frontend.js
+++ b/entry_types/scrolled/package/src/contentElements/question/frontend.js
@@ -2,5 +2,6 @@ import {frontend} from 'pageflow-scrolled/frontend';
import {Question} from './Question';
frontend.contentElementTypes.register('question', {
- component: Question
+ component: Question,
+ defaultMarginTop: '1.375rem'
});
diff --git a/entry_types/scrolled/package/src/contentElements/textBlock/TextBlock.module.css b/entry_types/scrolled/package/src/contentElements/textBlock/TextBlock.module.css
index 982b5dd7e3..44887fbd00 100644
--- a/entry_types/scrolled/package/src/contentElements/textBlock/TextBlock.module.css
+++ b/entry_types/scrolled/package/src/contentElements/textBlock/TextBlock.module.css
@@ -16,10 +16,19 @@
margin: 1.375rem 0 0 0;
}
+.text > p:first-child {
+ margin-top: 0;
+}
+
.text li {
margin: var(--theme-text-block-first-list-item-margin-top, 1.375rem) 0 0 0;
}
+.text > ol:first-child > li:first-child,
+.text > ul:first-child > li:first-child {
+ margin-top: 0;
+}
+
.text li + li {
margin-top: var(--theme-text-block-list-item-margin-top, 0.6875rem);
}
@@ -59,6 +68,10 @@
overflow: hidden;
}
+.text > blockquote:first-child {
+ margin-top: 0;
+}
+
.text blockquote::before,
.text blockquote::after {
font-family: var(--theme-quote-mark-font-family);
diff --git a/entry_types/scrolled/package/src/contentElements/textBlock/frontend.js b/entry_types/scrolled/package/src/contentElements/textBlock/frontend.js
index c7d7b9dd57..5d6c64e3f1 100644
--- a/entry_types/scrolled/package/src/contentElements/textBlock/frontend.js
+++ b/entry_types/scrolled/package/src/contentElements/textBlock/frontend.js
@@ -4,5 +4,6 @@ import {TextBlock} from './TextBlock';
frontend.contentElementTypes.register('textBlock', {
component: TextBlock,
customSelectionRect: true,
- supportsWrappingAroundFloats: true
+ supportsWrappingAroundFloats: true,
+ defaultMarginTop: '1.375rem'
});
diff --git a/entry_types/scrolled/package/src/editor/views/EditSectionPaddingsView.js b/entry_types/scrolled/package/src/editor/views/EditSectionPaddingsView.js
index 560e114bca..0d96e8cf56 100644
--- a/entry_types/scrolled/package/src/editor/views/EditSectionPaddingsView.js
+++ b/entry_types/scrolled/package/src/editor/views/EditSectionPaddingsView.js
@@ -3,6 +3,7 @@ import I18n from 'i18n-js';
import {EditConfigurationView, SeparatorView} from 'pageflow/editor';
import {SliderInputView, RadioButtonGroupInputView, CheckBoxInputView, SelectInputView} from 'pageflow/ui';
+import {getAppearanceSectionScopeName} from 'pageflow-scrolled/frontend';
import {SectionPaddingVisualizationView} from './inputs/SectionPaddingVisualizationView';
import {EditMotifAreaInputView} from './inputs/EditMotifAreaInputView';
import paddingTopIcon from './images/paddingTop.svg';
@@ -32,8 +33,12 @@ export const EditSectionPaddingsView = EditConfigurationView.extend({
const section = this.model;
const configuration = section.configuration;
- const [paddingTopValues, paddingTopTexts] = entry.getScale('sectionPaddingTop');
- const [paddingBottomValues, paddingBottomTexts] = entry.getScale('sectionPaddingBottom');
+ const [paddingTopValues, paddingTopTexts, paddingTopCssValues] = entry.getScale('sectionPaddingTop');
+ const [paddingBottomValues, paddingBottomTexts, paddingBottomCssValues] = entry.getScale('sectionPaddingBottom');
+
+ const appearance = configuration.get('appearance');
+ const defaultPaddingTop = getDefaultPaddingValue(entry, 'sectionDefaultPaddingTop', paddingTopValues, paddingTopCssValues, appearance);
+ const defaultPaddingBottom = getDefaultPaddingValue(entry, 'sectionDefaultPaddingBottom', paddingBottomValues, paddingBottomCssValues, appearance);
const hasPortrait = hasPortraitBackdrop(configuration);
@@ -42,7 +47,7 @@ export const EditSectionPaddingsView = EditConfigurationView.extend({
entry.unset('emulation_mode');
}
- paddingInputs(this, {entry, section, paddingTopValues, paddingTopTexts, paddingBottomValues, paddingBottomTexts});
+ paddingInputs(this, {entry, section, paddingTopValues, paddingTopTexts, paddingBottomValues, paddingBottomTexts, defaultPaddingTop, defaultPaddingBottom});
});
if (!hasPortrait) {
@@ -70,6 +75,7 @@ export const EditSectionPaddingsView = EditConfigurationView.extend({
prefix: usePortraitProperties ? 'portrait' : '',
portrait: true,
paddingTopValues, paddingTopTexts, paddingBottomValues, paddingBottomTexts,
+ defaultPaddingTop, defaultPaddingBottom,
disabledOptions: usePortraitProperties ? {} : {disabled: true}
});
});
@@ -83,6 +89,7 @@ function paddingInputs(tab, options) {
prefix = '',
portrait = false,
paddingTopValues, paddingTopTexts, paddingBottomValues, paddingBottomTexts,
+ defaultPaddingTop, defaultPaddingBottom,
disabledOptions
} = options;
@@ -166,6 +173,7 @@ function paddingInputs(tab, options) {
icon: paddingTopIcon,
texts: paddingTopTexts,
values: paddingTopValues,
+ defaultValue: defaultPaddingTop,
saveOnSlide: true,
onInteractionStart: scrollToSectionStart,
disabledBinding: motifAreaNotDefinedBinding,
@@ -185,6 +193,7 @@ function paddingInputs(tab, options) {
icon: paddingBottomIcon,
texts: paddingBottomTexts,
values: paddingBottomValues,
+ defaultValue: defaultPaddingBottom,
saveOnSlide: true,
onInteractionStart: scrollToSectionEnd,
...disabledOptions
@@ -228,3 +237,16 @@ function hasPortraitBackdrop(configuration) {
return !!configuration.getReference(propertyName, collection);
}
+
+function getDefaultPaddingValue(entry, propertyName, scaleValues, scaleCssValues, appearance) {
+ const properties = entry.getThemeProperties();
+ const scopeName = getAppearanceSectionScopeName(appearance);
+ const defaultCssValue = properties[scopeName]?.[propertyName] ?? properties.root?.[propertyName];
+
+ if (!defaultCssValue) {
+ return undefined;
+ }
+
+ const index = scaleCssValues.indexOf(defaultCssValue);
+ return index >= 0 ? scaleValues[index] : undefined;
+}
diff --git a/entry_types/scrolled/package/src/frontend/ContentElement.js b/entry_types/scrolled/package/src/frontend/ContentElement.js
index f078627b16..cc69c6aa49 100644
--- a/entry_types/scrolled/package/src/frontend/ContentElement.js
+++ b/entry_types/scrolled/package/src/frontend/ContentElement.js
@@ -13,6 +13,7 @@ export const ContentElement = React.memo(withInlineEditingDecorator(
'ContentElementDecorator',
function ContentElement(props) {
const Component = api.contentElementTypes.getComponent(props.type);
+ const {defaultMarginTop} = api.contentElementTypes.getOptions(props.type) || {};
if (Component) {
return (
@@ -20,6 +21,8 @@ export const ContentElement = React.memo(withInlineEditingDecorator(
{children}
diff --git a/entry_types/scrolled/package/src/frontend/ContentElementMargin.module.css b/entry_types/scrolled/package/src/frontend/ContentElementMargin.module.css
index 4b0a8968f9..9db2364c93 100644
--- a/entry_types/scrolled/package/src/frontend/ContentElementMargin.module.css
+++ b/entry_types/scrolled/package/src/frontend/ContentElementMargin.module.css
@@ -1,3 +1,7 @@
.wrapper {
margin: 1em 0 0 0;
}
+
+.noTopMargin {
+ margin-top: 0 !important;
+}
diff --git a/entry_types/scrolled/package/src/frontend/Foreground.js b/entry_types/scrolled/package/src/frontend/Foreground.js
index 56b7934128..135b1b1fae 100644
--- a/entry_types/scrolled/package/src/frontend/Foreground.js
+++ b/entry_types/scrolled/package/src/frontend/Foreground.js
@@ -23,7 +23,9 @@ function className(props, forcePadding) {
styles.Foreground,
props.transitionStyles.foreground,
props.transitionStyles[`foreground-${props.state}`],
- {[styles.paddingBottom]: props.paddingBottom || forcePadding},
+ {[styles.suppressedPaddingTop]: props.suppressedPaddings?.top},
+ {[styles.suppressedPaddingBottom]: props.suppressedPaddings?.bottom},
+ {[styles.forcePadding]: forcePadding},
styles[`${props.heightMode}Height`],
styles[spaceClassName(props.section?.remainingVerticalSpace)]
)
diff --git a/entry_types/scrolled/package/src/frontend/Foreground.module.css b/entry_types/scrolled/package/src/frontend/Foreground.module.css
index 00723d5111..ddf938031c 100644
--- a/entry_types/scrolled/package/src/frontend/Foreground.module.css
+++ b/entry_types/scrolled/package/src/frontend/Foreground.module.css
@@ -10,7 +10,8 @@
flex-direction: column;
justify-content: center;
- padding-top: var(--foreground-padding-top);
+ padding-top: calc(var(--foreground-widget-padding-top) + var(--foreground-padding-top));
+ padding-bottom: var(--foreground-padding-bottom);
}
.fullFadeHeight {
@@ -29,8 +30,16 @@
justify-content: flex-start;
}
-.paddingBottom {
- padding-bottom: var(--foreground-padding-bottom, 3em);
+.suppressedPaddingTop {
+ --foreground-padding-top: 0px;
+}
+
+.suppressedPaddingBottom {
+ --foreground-padding-bottom: 0px;
+}
+
+.forcePadding {
+ padding-bottom: max(var(--foreground-padding-bottom, 0px), 3em);
}
@media print {
diff --git a/entry_types/scrolled/package/src/frontend/Section.js b/entry_types/scrolled/package/src/frontend/Section.js
index 6cf64b52e7..26a9c0bb9b 100644
--- a/entry_types/scrolled/package/src/frontend/Section.js
+++ b/entry_types/scrolled/package/src/frontend/Section.js
@@ -22,13 +22,13 @@ import {BackgroundColorProvider} from './backgroundColor';
import {SelectableWidget} from './SelectableWidget';
import {useSectionPadding} from './useSectionPaddingCustomProperties';
import {SectionIntersectionProbe} from './SectionIntersectionObserver';
+import {getAppearanceComponents, getAppearanceSectionScopeName} from './appearance';
import * as v1 from './v1';
import * as v2 from './v2';
import styles from './Section.module.css';
import {getTransitionStyles, getEnterAndExitTransitions} from './transitions'
-import {getAppearanceComponents} from './appearance';
const Section = withInlineEditingDecorator('SectionDecorator', function Section({
section, transitions, backdrop, contentElements, state, onActivate, domIdPrefix
@@ -54,7 +54,7 @@ const Section = withInlineEditingDecorator('SectionDecorator', function Section(
propertyName: 'atmoAudioFileId'
});
- const sectionPadding = useSectionPadding(section);
+ const sectionPadding = useSectionPadding(section, {portrait: backdrop.portrait});
return (
+ sectionProps={sectionProperties}
+ isContentPadded={motifAreaState.isContentPadded}>
{(children, boxProps) =>
0},
{[styles.cardStart]: !props.openStart},
- {[styles.cardEnd]: !props.openEnd}
+ {[styles.cardEnd]: !props.openEnd},
+ {[boundaryMarginStyles.noTopMargin]: props.atSectionStart},
+ {[boundaryMarginStyles.noBottomMargin]: props.atSectionEnd}
);
}
diff --git a/entry_types/scrolled/package/src/frontend/foregroundBoxes/InvisibleBoxWrapper.js b/entry_types/scrolled/package/src/frontend/foregroundBoxes/InvisibleBoxWrapper.js
index 230b5a737f..565476bf82 100644
--- a/entry_types/scrolled/package/src/frontend/foregroundBoxes/InvisibleBoxWrapper.js
+++ b/entry_types/scrolled/package/src/frontend/foregroundBoxes/InvisibleBoxWrapper.js
@@ -2,17 +2,23 @@ import React from 'react';
import classNames from 'classnames';
import {widths} from '../layouts';
+import {TrimMarginTopProvider} from '../TrimMarginTop';
import styles from './InvisibleBoxWrapper.module.css';
+import boundaryMarginStyles from './BoxBoundaryMargin.module.css';
-export function InvisibleBoxWrapper({position, width, openStart, openEnd, children}) {
+export function InvisibleBoxWrapper({position, width, openStart, openEnd, atSectionStart, atSectionEnd, children}) {
const full = (width === widths.full);
return (
-
- {children}
-
+
+
+ {children}
+
+
)
}
diff --git a/entry_types/scrolled/package/src/frontend/index.js b/entry_types/scrolled/package/src/frontend/index.js
index e6edfa0dc9..52f737b40c 100644
--- a/entry_types/scrolled/package/src/frontend/index.js
+++ b/entry_types/scrolled/package/src/frontend/index.js
@@ -126,6 +126,7 @@ export {ScrollButton} from './ScrollButton';
export {textColorForBackgroundColor} from './textColorForBackgroundColor';
export {getTransitionNames, getAvailableTransitionNames} from './transitions';
+export {getAppearanceSectionScopeName} from './appearance';
export {RootProviders, registerConsentVendors};
export {default as registerTemplateWidgetType} from './registerTemplateWidgetType';
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js b/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js
index b1cdc989eb..a466ac26de 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js
@@ -4,7 +4,7 @@ import {features} from 'pageflow/frontend';
import {PaddingIndicator} from './PaddingIndicator';
-export function ForegroundDecorator({section, motifAreaState, sectionPadding, children}) {
+export function ForegroundDecorator({section, motifAreaState, sectionPadding, suppressedPaddings, children}) {
if (!features.isEnabled('section_paddings')) {
return children;
}
@@ -14,10 +14,12 @@ export function ForegroundDecorator({section, motifAreaState, sectionPadding, ch
{children}
>
);
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.js b/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.js
index 4b577662de..357f5e8760 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.js
@@ -20,7 +20,7 @@ const scaleNames = {
bottom: 'sectionPaddingBottom'
};
-export function PaddingIndicator({section, motifAreaState, paddingValue, position}) {
+export function PaddingIndicator({section, motifAreaState, paddingValue, position, suppressed}) {
const Icon = paddingIcons[position];
const theme = useTheme();
const {t} = useI18n({locale: 'ui'});
@@ -39,7 +39,7 @@ export function PaddingIndicator({section, motifAreaState, paddingValue, positio
const motifPadding = motifAreaState?.paddingTop > 0;
const paddingText = motifPadding ?
t('pageflow_scrolled.inline_editing.expose_motif_area') :
- getPaddingText({theme, paddingValue, position, t});
+ getPaddingText({theme, paddingValue, position, t, suppressed});
if (isSectionSelected || isPaddingSelected) {
return (
@@ -47,7 +47,7 @@ export function PaddingIndicator({section, motifAreaState, paddingValue, positio
className={classNames(styles[`indicator-${position}`],
{[styles.selected]: isPaddingSelected,
[styles.motif]: motifPadding,
- [styles.none]: !motifPadding && paddingValue === 'none'})}
+ [styles.none]: !motifPadding && (paddingValue === 'none' || suppressed)})}
onClick={() => select()}>
@@ -61,7 +61,14 @@ export function PaddingIndicator({section, motifAreaState, paddingValue, positio
}
}
-function getPaddingText({theme, paddingValue, position, t}) {
+function getPaddingText({theme, paddingValue, position, t, suppressed}) {
+ if (suppressed) {
+ const key = position === 'top' ?
+ 'pageflow_scrolled.inline_editing.padding_suppressed_before_full_width' :
+ 'pageflow_scrolled.inline_editing.padding_suppressed_after_full_width';
+ return t(key);
+ }
+
if (!paddingValue) {
return t('pageflow_scrolled.inline_editing.default_padding');
}
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.module.css b/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.module.css
index 82a21fb34c..c1410d8961 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.module.css
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/PaddingIndicator.module.css
@@ -36,7 +36,7 @@
.indicator-top {
composes: indicator;
- top: 0;
+ top: var(--foreground-widget-padding-top);
height: var(--foreground-padding-top, space(12));
}
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.js b/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.js
index 9f96f3d6b0..6516691534 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.js
@@ -80,7 +80,7 @@ export function SectionDecorator({backdrop, section, contentElements, transition
position: 'after'})}
-
+
{children}
@@ -88,9 +88,9 @@ export function SectionDecorator({backdrop, section, contentElements, transition
);
}
-function className(isSectionSelected, transitionSelection, isHighlighted, isBackdropElementSelected, transitions) {
+function className(isSelected, transitionSelection, isHighlighted, isBackdropElementSelected, transitions) {
return classNames(styles.wrapper, {
- [styles.selected]: isSectionSelected,
+ [styles.selected]: isSelected,
[styles.highlighted]: isHighlighted,
[styles.lineAbove]: isBackdropElementSelected && transitions[0].startsWith('fade'),
[styles.lineBelow]: isBackdropElementSelected && transitions[1].startsWith('fade'),
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.module.css b/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.module.css
index a110c5f8ea..d1df364857 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.module.css
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/SectionDecorator.module.css
@@ -6,7 +6,7 @@ section::before {
content: "";
display: block;
position: absolute;
- top: 6px;
+ top: calc(6px + var(--foreground-widget-padding-top));
left: 6px;
right: 6px;
bottom: 6px;
diff --git a/entry_types/scrolled/package/src/frontend/layouts/Center.js b/entry_types/scrolled/package/src/frontend/layouts/Center.js
index 22760d7f5c..bbff72caa6 100644
--- a/entry_types/scrolled/package/src/frontend/layouts/Center.js
+++ b/entry_types/scrolled/package/src/frontend/layouts/Center.js
@@ -34,7 +34,7 @@ export function Center(props) {
{[styles[`sideBySide`]]: sideBySideFloat(props.items, index)})}>
{child}
,
- boxProps(props.items, item, index)
+ boxProps(props.items, item, index, props.isContentPadded)
)}
@@ -58,7 +58,7 @@ function outerClassName(items, index) {
);
}
-function boxProps(items, item, index) {
+function boxProps(items, item, index, isContentPadded) {
const previous = items[index - 1];
const next = items[index + 1];
const customMargin = hasCustomMargin(item);
@@ -76,7 +76,9 @@ function boxProps(items, item, index) {
openEnd: next &&
!customMargin &&
!hasCustomMargin(next) &&
- !isWideOrFull(item) && !isWideOrFull(next)
+ !isWideOrFull(item) && !isWideOrFull(next),
+ atSectionStart: index === 0 && !isContentPadded,
+ atSectionEnd: index === items.length - 1
}
}
diff --git a/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js b/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js
index b6b72808aa..a9c52ac88d 100644
--- a/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js
+++ b/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js
@@ -51,18 +51,21 @@ TwoColumn.GroupComponent = 'div';
TwoColumn.contentAreaProbeProps = {};
function renderItems(props, shouldInline) {
- return groupItemsByPosition(props.items, shouldInline).map((group, index) =>
-
+
- {group.boxes.map((box, index) => renderItemGroup(props, box, index))}
+ {group.boxes.map((box, boxIndex) =>
+ renderItemGroup(props, box, boxIndex)
+ )}
);
}
function renderItemGroup(props, box, key) {
if (box.items.length) {
-
return (
@@ -101,10 +106,11 @@ function RestrictWidth({width, alignment, children}) {
}
}
-function groupItemsByPosition(items, shouldInline) {
+function groupItemsByPosition(items, shouldInline, isContentPadded) {
const groups = [];
let lastInlineBox = null;
+ let firstInlineBox = null;
let currentGroup, currentBox;
items.reduce((previousPosition, item, index) => {
@@ -161,6 +167,11 @@ function groupItemsByPosition(items, shouldInline) {
lastInlineBox = null;
}
+ if (position === 'inline' && !firstInlineBox) {
+ currentBox.atSectionStart = !isContentPadded;
+ firstInlineBox = currentBox;
+ }
+
currentGroup.boxes.push(currentBox)
}
@@ -168,6 +179,10 @@ function groupItemsByPosition(items, shouldInline) {
return position;
}, null);
+ if (currentBox) {
+ currentBox.atSectionEnd = true;
+ }
+
return groups;
}
diff --git a/entry_types/scrolled/package/src/frontend/layouts/index.js b/entry_types/scrolled/package/src/frontend/layouts/index.js
index 29ca1f5078..2df9f09545 100644
--- a/entry_types/scrolled/package/src/frontend/layouts/index.js
+++ b/entry_types/scrolled/package/src/frontend/layouts/index.js
@@ -14,7 +14,8 @@ export const Layout = React.memo(
prevProps.items === nextProps.items &&
prevProps.appearance === nextProps.appearance &&
prevProps.contentAreaRef === nextProps.contentAreaRef &&
- prevProps.sectionProps === nextProps.sectionProps
+ prevProps.sectionProps === nextProps.sectionProps &&
+ prevProps.isContentPadded === nextProps.isContentPadded
)
);
diff --git a/entry_types/scrolled/package/src/frontend/useSectionPaddingCustomProperties.js b/entry_types/scrolled/package/src/frontend/useSectionPaddingCustomProperties.js
index f6e9c6c854..1b0f2d9626 100644
--- a/entry_types/scrolled/package/src/frontend/useSectionPaddingCustomProperties.js
+++ b/entry_types/scrolled/package/src/frontend/useSectionPaddingCustomProperties.js
@@ -1,17 +1,13 @@
-import {usePortraitOrientation} from './usePortraitOrientation';
-
-export function useSectionPadding(section) {
- const portrait = usePortraitOrientation({
- active: section.portraitPaddingTop ||
- section.portraitPaddingBottom
- });
+export function useSectionPadding(section, {portrait} = {}) {
+ const usePortraitPaddings =
+ section.customPortraitPaddings !== false && portrait;
const paddingTop =
- portrait && section.portraitPaddingTop ?
+ usePortraitPaddings && section.portraitPaddingTop ?
section.portraitPaddingTop : section.paddingTop;
const paddingBottom =
- portrait && section.portraitPaddingBottom ?
+ usePortraitPaddings && section.portraitPaddingBottom ?
section.portraitPaddingBottom : section.paddingBottom;
const styles = {};
diff --git a/entry_types/scrolled/package/src/frontend/v1/__mocks__/useMotifAreaState.js b/entry_types/scrolled/package/src/frontend/v1/__mocks__/useMotifAreaState.js
index fceb46aac9..cb801d4802 100644
--- a/entry_types/scrolled/package/src/frontend/v1/__mocks__/useMotifAreaState.js
+++ b/entry_types/scrolled/package/src/frontend/v1/__mocks__/useMotifAreaState.js
@@ -1,28 +1,41 @@
-let currentState = {};
+import {useState, useCallback} from 'react';
+
+let initialState = {};
+let setStateRef = null;
export function useMotifAreaState() {
+ const [overrides, setOverrides] = useState(initialState);
+ setStateRef = setOverrides;
+
const state = {
paddingTop: 0,
isContentPadded: false,
minHeight: undefined,
intersectionRatioY: 0,
isMotifIntersected: false,
- ...currentState
+ ...overrides
};
- const setMotifAreaRef = () => {};
- const setContentAreaRef = () => {};
+ const setMotifAreaRef = useCallback(() => {}, []);
+ const setContentAreaRef = useCallback(() => {}, []);
return [state, setMotifAreaRef, setContentAreaRef];
}
beforeEach(() => {
- currentState = {};
+ initialState = {};
+ setStateRef = null;
});
useMotifAreaState.mockContentPadded = function() {
- currentState = {
+ const state = {
paddingTop: 100,
isContentPadded: true
};
+
+ if (setStateRef) {
+ setStateRef(state);
+ } else {
+ initialState = state;
+ }
};
diff --git a/entry_types/scrolled/package/src/frontend/v1/useBackdrop.js b/entry_types/scrolled/package/src/frontend/v1/useBackdrop.js
index 148befb6a9..e92f589fec 100644
--- a/entry_types/scrolled/package/src/frontend/v1/useBackdrop.js
+++ b/entry_types/scrolled/package/src/frontend/v1/useBackdrop.js
@@ -53,14 +53,16 @@ function useFileBackdrop({section, collectionName, propertyName}) {
return {
[propertyName]: mobileFile,
motifArea: section.backdrop[`${propertyName}MobileMotifArea`],
- effects: section.backdropEffectsMobile
+ effects: section.backdropEffectsMobile,
+ portrait: true
}
}
else if (file) {
return {
[propertyName]: file,
motifArea: section.backdrop[`${propertyName}MotifArea`],
- effects: section.backdropEffects
+ effects: section.backdropEffects,
+ portrait: false
}
}
else {
diff --git a/entry_types/scrolled/spec/pageflow_scrolled/theme_options_default_scale_spec.rb b/entry_types/scrolled/spec/pageflow_scrolled/theme_options_default_scale_spec.rb
new file mode 100644
index 0000000000..e3ca15c702
--- /dev/null
+++ b/entry_types/scrolled/spec/pageflow_scrolled/theme_options_default_scale_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+module PageflowScrolled
+ RSpec.describe ThemeOptionsDefaultScale do
+ it 'adds scale defaults if no scale properties defined' do
+ transform = ThemeOptionsDefaultScale.new(
+ prefix: 'section_padding_top',
+ values: {'none' => '0', 'sm' => '1em'}
+ )
+
+ result = transform.call({})
+
+ expect(result).to eq(
+ properties: {
+ root: {
+ 'section_padding_top-none' => '0',
+ 'section_padding_top-sm' => '1em'
+ }
+ }
+ )
+ end
+
+ it 'preserves existing theme options' do
+ transform = ThemeOptionsDefaultScale.new(
+ prefix: 'section_padding_top',
+ values: {'none' => '0', 'sm' => '1em'}
+ )
+
+ result = transform.call(colors: {accent: '#f00'})
+
+ expect(result).to eq(
+ colors: {accent: '#f00'},
+ properties: {
+ root: {
+ 'section_padding_top-none' => '0',
+ 'section_padding_top-sm' => '1em'
+ }
+ }
+ )
+ end
+
+ it 'does not add defaults if theme defines any scale property' do
+ transform = ThemeOptionsDefaultScale.new(
+ prefix: 'section_padding_top',
+ values: {'none' => '0', 'sm' => '1em', 'md' => '2em'}
+ )
+
+ result = transform.call(
+ properties: {
+ root: {
+ 'section_padding_top-lg' => '5em'
+ }
+ }
+ )
+
+ expect(result).to eq(
+ properties: {
+ root: {
+ 'section_padding_top-lg' => '5em'
+ }
+ }
+ )
+ end
+
+ it 'adds defaults for one scale even if other scale is defined' do
+ transform = ThemeOptionsDefaultScale.new(
+ prefix: 'section_padding_top',
+ values: {'none' => '0', 'sm' => '1em'}
+ )
+
+ result = transform.call(
+ properties: {
+ root: {
+ 'section_padding_bottom-lg' => '5em'
+ }
+ }
+ )
+
+ expect(result).to eq(
+ properties: {
+ root: {
+ 'section_padding_bottom-lg' => '5em',
+ 'section_padding_top-none' => '0',
+ 'section_padding_top-sm' => '1em'
+ }
+ }
+ )
+ end
+ end
+end
diff --git a/lib/pageflow/themes.rb b/lib/pageflow/themes.rb
index dc1386d782..b42182370c 100644
--- a/lib/pageflow/themes.rb
+++ b/lib/pageflow/themes.rb
@@ -4,6 +4,38 @@ class Themes # rubocop:todo Style/Documentation
def initialize
@themes = HashWithIndifferentAccess.new
+ @options_transforms = []
+ end
+
+ # Register default options that apply to all themes. Can be called
+ # multiple times to accumulate defaults from different sources
+ # (gem, plugins, host app).
+ #
+ # @overload register_default_options(options)
+ # @param options [Hash]
+ # Default options to deep merge into theme options.
+ #
+ # @overload register_default_options(callable)
+ # @param callable [#call]
+ # Receives options hash, returns transformed options.
+ # Use for conditional defaults based on what theme defines.
+ #
+ # @since edge
+ def register_default_options(options_or_callable)
+ @options_transforms << if options_or_callable.respond_to?(:call)
+ options_or_callable
+ else
+ ->(options) { options_or_callable.deep_merge(options) }
+ end
+ end
+
+ # Apply all registered defaults to theme options.
+ #
+ # @api private
+ def apply_default_options(options)
+ @options_transforms.reduce(options.deep_dup) do |opts, transform|
+ transform.call(opts)
+ end
end
# Register a theme and supply theme options.
diff --git a/spec/pageflow/theme_customizations_spec.rb b/spec/pageflow/theme_customizations_spec.rb
index 2e4a39e042..c4ccd60d1e 100644
--- a/spec/pageflow/theme_customizations_spec.rb
+++ b/spec/pageflow/theme_customizations_spec.rb
@@ -471,5 +471,122 @@ module Pageflow
expect(customization.selected_files[:inverted_logo].urls[:small])
.to match(%r{small/image.png})
end
+
+ it 'deep merges hash default options into theme options' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_default_options(colors: {accent: '#default', surface: '#fff'})
+ c.themes.register('dark')
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ expect(entry.theme.options).to match(colors: {accent: '#default', surface: '#fff'})
+ end
+
+ it 'lets theme options override hash defaults' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_default_options(colors: {accent: '#default', surface: '#fff'})
+ c.themes.register('dark', colors: {accent: '#f00'})
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ expect(entry.theme.options).to match(colors: {accent: '#f00', surface: '#fff'})
+ end
+
+ it 'allows site customizations to override default options' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_default_options(colors: {surface: '#fff'})
+ c.themes.register('dark', colors: {accent: '#f00'})
+ end
+ end
+ site = create(:site)
+ entry = create(:published_entry,
+ site:,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ Pageflow.theme_customizations.update(site:,
+ entry_type_name: 'rainbow',
+ overrides: {colors: {accent: '#0f0'}})
+
+ expect(entry.theme.options).to match(colors: {accent: '#0f0', surface: '#fff'})
+ end
+
+ it 'accumulates multiple register_default_options calls' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_default_options(colors: {accent: '#default'})
+ c.themes.register_default_options(typography: {base: {fontSize: '16px'}})
+ c.themes.register('dark')
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ expect(entry.theme.options).to match(
+ colors: {accent: '#default'},
+ typography: {base: {fontSize: '16px'}}
+ )
+ end
+
+ it 'supports callable for conditional defaults' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_default_options(->(options) {
+ if options[:colors].blank?
+ options.deep_merge(colors: {accent: '#default'})
+ else
+ options
+ end
+ })
+ c.themes.register('dark', colors: {accent: '#f00'})
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ expect(entry.theme.options).to match(colors: {accent: '#f00'})
+ end
+
+ it 'scopes default options by entry type' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+ other_entry_type = TestEntryType.register(config, name: 'other')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register('dark', colors: {accent: '#f00'})
+ end
+
+ config.for_entry_type(other_entry_type) do |c|
+ c.themes.register_default_options(colors: {accent: '#other'})
+ c.themes.register('dark')
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ expect(entry.theme.options).to match(colors: {accent: '#f00'})
+ end
end
end
diff --git a/spec/support/pageflow/shared_contexts/fake_translations.rb b/spec/support/pageflow/shared_contexts/fake_translations.rb
index 03771bba45..0f56fe57fe 100644
--- a/spec/support/pageflow/shared_contexts/fake_translations.rb
+++ b/spec/support/pageflow/shared_contexts/fake_translations.rb
@@ -4,6 +4,7 @@
@fake_i18n_backend = I18n::Backend::Simple.new
I18n.backend = @fake_i18n_backend
+ @fake_i18n_backend.send(:init_translations)
example.run
I18n.backend = i18n_backend_backup
end