diff --git a/entry_types/scrolled/config/locales/new/heading_subtitles.de.yml b/entry_types/scrolled/config/locales/new/heading_subtitles.de.yml new file mode 100644 index 000000000..e346b2513 --- /dev/null +++ b/entry_types/scrolled/config/locales/new/heading_subtitles.de.yml @@ -0,0 +1,17 @@ +de: + pageflow_scrolled: + inline_editing: + type_subtitle: "Untertitel eingeben" + type_tagline: "Tagline eingeben" + editor: + content_elements: + heading: + attributes: + entranceAnimation: + inline_help: Lege die Animation fest, mit der die Überschrift eingeblendet werden soll, wenn sie die Mitte des Viewports erreicht. + label: Eingangsanimation + values: + none: "(Keine)" + fadeIn: Einblenden + fadeInFast: Einblenden (Schnell) + fadeInSlow: Einblenden (Langsam) diff --git a/entry_types/scrolled/config/locales/new/heading_subtitles.en.yml b/entry_types/scrolled/config/locales/new/heading_subtitles.en.yml new file mode 100644 index 000000000..66b217084 --- /dev/null +++ b/entry_types/scrolled/config/locales/new/heading_subtitles.en.yml @@ -0,0 +1,17 @@ +en: + pageflow_scrolled: + inline_editing: + type_subtitle: "Type subtitle" + type_tagline: "Type tagline" + editor: + content_elements: + heading: + attributes: + entranceAnimation: + inline_help: Determine how the heading becomes visible once it reaches the center of the viewport + label: Entrance Animation + values: + fadeIn: Fade in + fadeInFast: Fade in (Fast) + fadeInSlow: Fade in (Slow) + none: "(None)" diff --git a/entry_types/scrolled/package/src/contentElements/heading/Heading.js b/entry_types/scrolled/package/src/contentElements/heading/Heading.js index 6d0119316..2da932340 100644 --- a/entry_types/scrolled/package/src/contentElements/heading/Heading.js +++ b/entry_types/scrolled/package/src/contentElements/heading/Heading.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect, useState, useRef} from 'react'; import classNames from 'classnames'; import { withShadowClassName, @@ -6,9 +6,13 @@ import { Text, EditableInlineText, useContentElementConfigurationUpdate, + useContentElementEditorState, + useContentElementLifecycle, useDarkBackground, + useIsStaticPreview, useI18n, - contentElementWidths + contentElementWidths, + utils } from 'pageflow-scrolled/frontend'; import styles from './Heading.module.css'; @@ -19,47 +23,118 @@ export function Heading({configuration, sectionProps, contentElementWidth}) { const updateConfiguration = useContentElementConfigurationUpdate(); const {t} = useI18n({locale: 'ui'}); const darkBackground = useDarkBackground(); + const {isSelected, isEditable} = useContentElementEditorState(); const legacyValue = configuration.children; const Tag = firstSectionInEntry ? 'h1' : 'h2'; const forcePaddingTop = firstSectionInEntry && !('marginTop' in configuration); - return ( - contentElementWidths.md || - sectionProps.layout === 'centerRagged'}, - {[withShadowClassName]: !sectionProps.invert})} - style={{color: paletteColor(configuration.color)}}> - - updateConfiguration({value})} /> + const entranceAnimation = (!useIsStaticPreview() && configuration.entranceAnimation) || 'none'; + const [animating, setAnimating] = useState(false); + + useContentElementLifecycle({ + onActivate() { + setAnimating(entranceAnimation !== 'none'); + }, + + onInvisible() { + if (isEditable) { + setAnimating(false); + } + } + }); + + const previousAnimation = useRef(entranceAnimation); + const previouslySelected = useRef(isSelected); + + useEffect(() => { + if (isEditable && previousAnimation.current !== entranceAnimation) { + previousAnimation.current = entranceAnimation; + + setAnimating(false) + setTimeout(() => setAnimating(true), 100); + } + }, [entranceAnimation, isEditable]); + + useEffect(() => { + if (!previouslySelected.current && isSelected) { + setAnimating(true) + } + + previouslySelected.current = isSelected; + }, [isSelected]); + + function renderSubtitle(name) { + const value = configuration[name]; + + if (!isSelected && utils.isBlankEditableTextValue(value)) { + return null; + } + + return ( + + + updateConfiguration({[name]: value})} /> + - + ); + } + + return ( + contentElementWidths.md || + sectionProps.layout === 'centerRagged'}, + {[withShadowClassName]: !sectionProps.invert})}> + {renderSubtitle('tagline')} + + + updateConfiguration({value})} /> + + + {renderSubtitle('subtitle')} + ); } -function getScaleCategory(configuration, firstSectionInEntry) { +function getScaleCategory(configuration, firstSectionInEntry, suffix = '') { + const base = `heading${capitalize(suffix)}`; + switch (configuration.textSize) { case 'large': - return 'heading-lg'; + return `${base}-lg`; case 'medium': - return 'heading-md'; + return `${base}-md`; case 'small': - return 'heading-sm'; + return `${base}-sm`; default: - return firstSectionInEntry ? 'heading-lg' : 'heading-sm'; + return firstSectionInEntry ? `${base}-lg` : `${base}-sm`; } } + +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} 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 e81d800fc..6fc1996cb 100644 --- a/entry_types/scrolled/package/src/contentElements/heading/Heading.module.css +++ b/entry_types/scrolled/package/src/contentElements/heading/Heading.module.css @@ -4,9 +4,74 @@ ) from "pageflow-scrolled/values/colors.module.css"; .root { - margin-top: 0.2em; + margin-top: 0.3em; margin-bottom: 0; - padding-top: 0.3em; + padding-top: 0.45em; +} + +.animation-fadeIn { + --fade-in-duration: 2s; + --fade-in-delay: 0.4s; +} + +.animation-fadeInFast { + composes: animation-fadeIn; + --fade-in-duration: 1s; + --fade-in-delay: 0.4s; +} + +.animation-fadeInSlow { + composes: animation-fadeIn; + --fade-in-duration: 3s; + --fade-in-delay: 0.8s; +} + +.animation-fadeIn .main, +.animation-fadeIn .tagline, +.animation-fadeIn .subtitle { + opacity: 0; +} + +.animation-fadeIn.animating .main, +.animation-fadeIn.animating .tagline, +.animation-fadeIn.animating .subtitle { + transition: opacity var(--fade-in-duration) ease; + opacity: 1; +} + +.animation-fadeIn.animating .subtitle { + transition-delay: var(--fade-in-delay); +} + +.animation-fadeIn.hasTagline.animating .main { + transition-delay: var(--fade-in-delay); +} + +.animation-fadeIn.hasTagline.animating .subtitle { + transition-delay: calc(var(--fade-in-delay) * 2); +} + +.main { + margin: 0; +} + +.tagline { + margin-bottom: 0.8em; +} + +.subtitle { + margin-top: 0.6em; + margin-bottom: 2em; +} + +@media (max-width: 600px) { + .tagline { + margin-bottom: 0.4em; + } + + .subtitle { + margin-top: 0.4em; + } } @media (min-width: 951px) { diff --git a/entry_types/scrolled/package/src/contentElements/heading/editor.js b/entry_types/scrolled/package/src/contentElements/heading/editor.js index 3e01bdfdc..8deee29de 100644 --- a/entry_types/scrolled/package/src/contentElements/heading/editor.js +++ b/entry_types/scrolled/package/src/contentElements/heading/editor.js @@ -40,6 +40,9 @@ editor.contentElementTypes.register('heading', { model: modelDelegator, propertyName: 'color' }); + this.input('entranceAnimation', SelectInputView, { + values: ['none', 'fadeInSlow', 'fadeIn', 'fadeInFast'], + }); this.input('hyphens', SelectInputView, { values: ['auto', 'manual'] diff --git a/entry_types/scrolled/package/src/contentElements/heading/frontend.js b/entry_types/scrolled/package/src/contentElements/heading/frontend.js index 6d1ceaaf1..f5a927271 100644 --- a/entry_types/scrolled/package/src/contentElements/heading/frontend.js +++ b/entry_types/scrolled/package/src/contentElements/heading/frontend.js @@ -2,5 +2,6 @@ import {frontend} from 'pageflow-scrolled/frontend'; import {Heading} from './Heading'; frontend.contentElementTypes.register('heading', { - component: Heading + component: Heading, + lifecycle: true }); diff --git a/entry_types/scrolled/package/src/contentElements/heading/stories.js b/entry_types/scrolled/package/src/contentElements/heading/stories.js index 1763ff550..50aa7d889 100644 --- a/entry_types/scrolled/package/src/contentElements/heading/stories.js +++ b/entry_types/scrolled/package/src/contentElements/heading/stories.js @@ -1,5 +1,6 @@ import '../frontend'; import {storiesOfContentElement} from 'pageflow-scrolled/spec/support/stories'; +import {contentElementWidths} from 'pageflow-scrolled/frontend'; storiesOfContentElement(module, { typeName: 'heading', @@ -27,11 +28,60 @@ storiesOfContentElement(module, { } }, { - name: 'Small', + name: 'Small ', configuration: { textSize: 'small' } }, + { + name: 'With subtitles - Large', + configuration: { + tagline: [{type: 'heading', children: [{text: 'Some Tagline'}]}], + subtitle: [{type: 'heading', children: [{text: 'Some Subtitle'}]}], + width: contentElementWidths.xl, + textSize: 'large' + } + }, + { + name: 'With subtitles - Medium', + configuration: { + tagline: [{type: 'heading', children: [{text: 'Some Tagline'}]}], + subtitle: [{type: 'heading', children: [{text: 'Some Subtitle'}]}], + width: contentElementWidths.xl, + textSize: 'medium' + } + }, + { + name: 'With subtitles - Small', + configuration: { + tagline: [{type: 'heading', children: [{text: 'Some Tagline'}]}], + subtitle: [{type: 'heading', children: [{text: 'Some Subtitle'}]}], + width: contentElementWidths.xl, + textSize: 'small' + } + }, + { + name: 'With subtitles - Center', + sectionConfiguration: { + layout: 'center', + }, + configuration: { + tagline: [{type: 'heading', children: [{text: 'Some Tagline'}]}], + subtitle: [{type: 'heading', children: [{text: 'Some Subtitle'}]}], + width: contentElementWidths.xl + } + }, + { + name: 'With subtitles - Right', + sectionConfiguration: { + layout: 'right', + }, + configuration: { + tagline: [{type: 'heading', children: [{text: 'Some Tagline'}]}], + subtitle: [{type: 'heading', children: [{text: 'Some Subtitle'}]}], + width: contentElementWidths.xl + } + }, { name: 'With custom content text colors', themeOptions: { diff --git a/entry_types/scrolled/package/src/frontend/Text.js b/entry_types/scrolled/package/src/frontend/Text.js index 26c05a499..8f3013c27 100644 --- a/entry_types/scrolled/package/src/frontend/Text.js +++ b/entry_types/scrolled/package/src/frontend/Text.js @@ -8,11 +8,13 @@ import styles from './Text.module.css'; * * @param {Object} props * @param {string} props.scaleCategory - - * One of the styles `'heading-lg'`, `'heading-md'`, `'heading-sm'`, - * `'heading-xs'`, `'body'`, `'caption'`, `'question'`, - * `'quoteText-lg`', `'quoteText-md`', `'quoteText-sm`', `'quoteText-xs`', `'quoteAttribution`', - * `'counterNumber-lg`', `'counterNumber-md`', `'counterNumber-sm`', - * `'counterNumber-xs`', `'counterDescription`'. + * One of the styles `'heading-lg'`, `'heading-md'`, `'heading-sm'`,`'heading-xs'`, + * `'headingTagline-lg'`, `'headingTagline-md'`, `'headingTagline-sm'`, + * `'headingSubtitle-lg'`, `'headingSubtitle-md'`, `'headingSubtitle-sm'`, + * `'body'`, `'caption'`, `'question'`, + * `'quoteText-lg'`, `'quoteText-md'`, `'quoteText-sm'`, `'quoteText-xs'`, `'quoteAttribution'`, + * `'counterNumber-lg'`, `'counterNumber-md'`, `'counterNumber-sm'`, + * `'counterNumber-xs'`, `'counterDescription`'. * @param {string} [props.inline] - Render a span instread of a div. * @param {string} props.children - Nodes to render with specified typography. */ @@ -27,6 +29,8 @@ Text.propTypes = { inline: PropTypes.bool, scaleCategory: PropTypes.oneOf([ 'heading-lg', 'heading-md', 'heading-sm', 'heading-xs', + 'headingTagline-lg', 'headingTagline-md', 'headingTagline-sm', + 'headingSubtitle-lg', 'headingSubtitle-md', 'headingSubtitle-sm', 'quoteText-lg', 'quoteText-md', 'quoteText-sm', 'quoteText-xs', 'quoteAttribution', 'counterNumber-lg', 'counterNumber-md', 'counterNumber-sm', 'counterNumber-xs', 'counterDescription', diff --git a/entry_types/scrolled/package/src/frontend/Text.module.css b/entry_types/scrolled/package/src/frontend/Text.module.css index 28fb45db5..74fb43c64 100644 --- a/entry_types/scrolled/package/src/frontend/Text.module.css +++ b/entry_types/scrolled/package/src/frontend/Text.module.css @@ -39,6 +39,45 @@ margin-bottom: 0; } + +.headingTagline-lg { + composes: typography-headingTagline from global; + font-size: text-md; + line-height: 1.2; +} + +.headingTagline-md { + composes: typography-headingTagline from global; + font-size: text-md; + line-height: 1.2; +} + +.headingTagline-sm { + composes: typography-headingTagline from global; + font-size: text-base; + line-height: 1.4; +} + + +.headingSubtitle-lg { + composes: typography-headingSubtitle from global; + font-size: 44px; + line-height: 1.2; +} + +.headingSubtitle-md { + composes: typography-headingSubtitle from global; + font-size: 44px; + line-height: 1.2; +} + +.headingSubtitle-sm { + composes: typography-headingSubtitle from global; + font-size: 26px; + line-height: 1.2; +} + + .body { composes: typography-body from global; font-size: text-base; @@ -137,6 +176,30 @@ font-size: text-l; } + + .headingTagline-lg { + font-size: 26px; + line-height: 1.2; + } + + .headingTagline-md, + .headingTagline-sm { + font-size: text-base; + line-height: 1.4; + } + + .headingSubtitle-lg { + font-size: text-md; + line-height: 1.2; + } + + .headingSubtitle-md, + .headingSubtitle-sm { + font-size: 26px; + line-height: 1.2; + } + + .quoteText-lg { font-size: text-l; line-height: 1.1;