diff --git a/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js b/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js index 68422d69fc..60547c3922 100644 --- a/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js +++ b/packages/kg-default-nodes/lib/nodes/call-to-action/CallToActionNode.js @@ -8,15 +8,15 @@ export class CallToActionNode extends generateDecoratorNode({ properties: [ {name: 'layout', default: 'minimal'}, {name: 'textValue', default: '', wordCount: true}, - {name: 'showButton', default: false}, - {name: 'buttonText', default: ''}, + {name: 'showButton', default: true}, + {name: 'buttonText', default: 'Learn more'}, {name: 'buttonUrl', default: ''}, - {name: 'buttonColor', default: ''}, - {name: 'buttonTextColor', default: ''}, + {name: 'buttonColor', default: '#000000'}, // Where colour is customisable, we should use hex values + {name: 'buttonTextColor', default: '#ffffff'}, {name: 'hasSponsorLabel', default: true}, {name: 'sponsorLabel', default: '

SPONSORED

'}, - {name: 'backgroundColor', default: 'grey'}, - {name: 'imageUrl', default: null}, + {name: 'backgroundColor', default: 'grey'}, // Since this is one of a few fixed options, we stick to colour names. + {name: 'imageUrl', default: ''}, {name: 'imageWidth', default: null}, {name: 'imageHeight', default: null} ] diff --git a/packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js b/packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js index 61e9abbe91..337682c651 100644 --- a/packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js +++ b/packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js @@ -33,8 +33,7 @@ function ctaCardTemplate(dataset) { ` : ''} ${dataset.showButton ? ` - + ${dataset.buttonText} ` : ''} @@ -46,12 +45,12 @@ function ctaCardTemplate(dataset) { } function emailCTATemplate(dataset, options = {}) { - const buttonStyle = dataset.buttonColor === 'accent' - ? `color: ${dataset.buttonTextColor};` + const buttonStyle = dataset.buttonColor === 'accent' + ? `color: ${dataset.buttonTextColor};` : `background-color: ${dataset.buttonColor}; color: ${dataset.buttonTextColor};`; let imageDimensions; - + if (dataset.imageUrl && dataset.imageWidth && dataset.imageHeight) { imageDimensions = { width: dataset.imageWidth, @@ -98,9 +97,10 @@ function emailCTATemplate(dataset, options = {}) { @@ -146,9 +146,10 @@ function emailCTATemplate(dataset, options = {}) {
- + style="${buttonStyle}" + > ${dataset.buttonText}
diff --git a/packages/kg-default-nodes/test/nodes/call-to-action.test.js b/packages/kg-default-nodes/test/nodes/call-to-action.test.js index ea86f0f471..e844c478ad 100644 --- a/packages/kg-default-nodes/test/nodes/call-to-action.test.js +++ b/packages/kg-default-nodes/test/nodes/call-to-action.test.js @@ -81,11 +81,11 @@ describe('CallToActionNode', function () { callToActionNode.textValue = 'This is a cool advertisement'; callToActionNode.textValue.should.equal('This is a cool advertisement'); - callToActionNode.showButton.should.equal(false); - callToActionNode.showButton = true; callToActionNode.showButton.should.equal(true); + callToActionNode.showButton = false; + callToActionNode.showButton.should.equal(false); - callToActionNode.buttonText.should.equal(''); + callToActionNode.buttonText.should.equal('Learn more'); callToActionNode.buttonText = 'click me'; callToActionNode.buttonText.should.equal('click me'); @@ -97,11 +97,11 @@ describe('CallToActionNode', function () { callToActionNode.sponsorLabel = 'This post is brought to you by our sponsors'; callToActionNode.sponsorLabel.should.equal('This post is brought to you by our sponsors'); - callToActionNode.buttonColor.should.equal(''); - callToActionNode.buttonColor = 'red'; - callToActionNode.buttonColor.should.equal('red'); + callToActionNode.buttonColor.should.equal('#000000'); + callToActionNode.buttonColor = '#ffffff'; + callToActionNode.buttonColor.should.equal('#ffffff'); - callToActionNode.buttonTextColor.should.equal(''); + callToActionNode.buttonTextColor.should.equal('#ffffff'); callToActionNode.buttonTextColor = 'black'; callToActionNode.buttonTextColor.should.equal('black'); @@ -110,10 +110,10 @@ describe('CallToActionNode', function () { callToActionNode.hasSponsorLabel.should.equal(false); callToActionNode.backgroundColor.should.equal('grey'); - callToActionNode.backgroundColor = '#654321'; - callToActionNode.backgroundColor.should.equal('#654321'); + callToActionNode.backgroundColor = 'red'; + callToActionNode.backgroundColor.should.equal('red'); - should(callToActionNode.imageUrl).be.null(); + callToActionNode.imageUrl.should.equal(''); callToActionNode.imageUrl = 'http://blog.com/image1.jpg'; callToActionNode.imageUrl.should.equal('http://blog.com/image1.jpg'); @@ -259,7 +259,6 @@ describe('CallToActionNode', function () { buttonText: 'Get access now', buttonTextColor: '#000000', buttonUrl: 'http://someblog.com/somepost', - hasImage: true, hasSponsorLabel: true, sponsorLabel: '

SPONSORED

', imageUrl: '/content/images/2022/11/koenig-lexical.jpg', @@ -276,7 +275,7 @@ describe('CallToActionNode', function () { html.should.containEql('Get access now'); html.should.containEql('http://someblog.com/somepost'); html.should.containEql('

SPONSORED

'); // because hasSponsorLabel is true - html.should.containEql('/content/images/size/w64h64/2022/11/koenig-lexical.jpg'); // because hasImage is true + html.should.containEql('/content/images/size/w64h64/2022/11/koenig-lexical.jpg'); html.should.containEql('This is a new CTA Card via email.'); })); @@ -289,7 +288,6 @@ describe('CallToActionNode', function () { buttonText: 'Get access now', buttonTextColor: '#000000', buttonUrl: 'http://someblog.com/somepost', - hasImage: true, hasSponsorLabel: true, sponsorLabel: '

SPONSORED

', imageUrl: '/content/images/2022/11/koenig-lexical.jpg', @@ -301,7 +299,7 @@ describe('CallToActionNode', function () { const {element} = callToActionNode.exportDOM(exportOptions); const html = element.outerHTML.toString(); - html.should.containEql('/content/images/size/w256h256/2022/11/koenig-lexical.jpg'); // because hasImage is true + html.should.containEql('/content/images/size/w256h256/2022/11/koenig-lexical.jpg'); })); it('renders email with img width and height when immersive', editorTest(function () { diff --git a/packages/koenig-lexical/src/components/ui/ButtonGroupBeta.jsx b/packages/koenig-lexical/src/components/ui/ButtonGroupBeta.jsx new file mode 100644 index 0000000000..d6361b8712 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/ButtonGroupBeta.jsx @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import {Tooltip} from './Tooltip'; +import {usePreviousFocus} from '../../hooks/usePreviousFocus'; + +export function ButtonGroupBeta({buttons = [], selectedName, onClick, hasTooltip = true}) { + return ( +
+ +
+ ); +} + +export function ButtonGroupIconButton({dataTestId, onClick, label, ariaLabel, name, selectedName, Icon, hasTooltip}) { + const isActive = name === selectedName; + + const {handleMousedown, handleClick} = usePreviousFocus(onClick, name); + + return ( +
  • + +
  • + ); +} + +ButtonGroupBeta.propTypes = { + selectedName: PropTypes.oneOf(['regular', 'wide', 'full', 'split', 'center', 'left', 'small', 'medium', 'large', 'grid', 'list', 'minimal', 'immersive']).isRequired, + hasTooltip: PropTypes.bool, + onClick: PropTypes.func.isRequired, + buttons: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string, + name: PropTypes.string.isRequired, + Icon: PropTypes.elementType, + dataTestId: PropTypes.string, + ariaLabel: PropTypes.string + })) +}; diff --git a/packages/koenig-lexical/src/components/ui/ButtonGroupBeta.stories.jsx b/packages/koenig-lexical/src/components/ui/ButtonGroupBeta.stories.jsx new file mode 100644 index 0000000000..dd94b281a9 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/ButtonGroupBeta.stories.jsx @@ -0,0 +1,48 @@ +import ImgFullIcon from '../../assets/icons/kg-img-full.svg?react'; +import ImgRegularIcon from '../../assets/icons/kg-img-regular.svg?react'; +import ImgWideIcon from '../../assets/icons/kg-img-wide.svg?react'; +import React from 'react'; +import {ButtonGroupBeta, ButtonGroupIconButton} from './ButtonGroupBeta'; + +const story = { + title: 'Generic/Button group (beta)', + component: ButtonGroupBeta, + subcomponents: {ButtonGroupIconButton}, + parameters: { + status: { + type: 'functional' + } + }, + argTypes: { + selectedName: {control: 'select', options: ['regular', 'wide', 'full']} + } +}; +export default story; + +const Template = (args) => { + return ( + + ); +}; + +export const CardWidth = Template.bind({}); +CardWidth.args = { + selectedName: 'regular', + buttons: [ + { + label: 'Regular', + name: 'regular', + Icon: ImgRegularIcon + }, + { + label: 'Wide', + name: 'wide', + Icon: ImgWideIcon + }, + { + label: 'Full', + name: 'full', + Icon: ImgFullIcon + } + ] +}; diff --git a/packages/koenig-lexical/src/components/ui/ColorOptionButtonsBeta.jsx b/packages/koenig-lexical/src/components/ui/ColorOptionButtonsBeta.jsx new file mode 100644 index 0000000000..d361a39796 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/ColorOptionButtonsBeta.jsx @@ -0,0 +1,93 @@ +import PlusIcon from '../../assets/icons/plus.svg?react'; +import React, {useState} from 'react'; +import {Tooltip} from './Tooltip'; +import {useClickOutside} from '../../hooks/useClickOutside'; +import {usePreviousFocus} from '../../hooks/usePreviousFocus'; + +export function ColorOptionButtonsBeta({buttons = [], selectedName, onClick}) { + const [isOpen, setIsOpen] = useState(false); + const componentRef = React.useRef(null); + + const selectedButton = buttons.find(button => button.name === selectedName); + + // Close the swatch popover when clicking outside of it + useClickOutside(isOpen, componentRef, () => setIsOpen(false)); + + return ( +
    + + + {/* Color options popover */} + {isOpen && ( +
    +
    +
      + {buttons.map(({label, name, color}) => ( + name !== 'image' ? + { + onClick(title); + setIsOpen(false); + }} + /> + : +
    • onClick(name)}> + + + +
    • + ))} +
    +
    +
    + )} +
    + ); +} + +export function ColorButton({onClick, label, name, color, selectedName}) { + const isActive = name === selectedName; + + const {handleMousedown, handleClick} = usePreviousFocus(onClick, name); + return ( +
  • + +
  • + ); +} diff --git a/packages/koenig-lexical/src/components/ui/ColorPickerBeta.jsx b/packages/koenig-lexical/src/components/ui/ColorPickerBeta.jsx new file mode 100644 index 0000000000..73d5d62095 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/ColorPickerBeta.jsx @@ -0,0 +1,290 @@ +import EyedropperIcon from '../../assets/icons/kg-eyedropper.svg?react'; +import ImgBgIcon from '../../assets/icons/kg-img-bg.svg?react'; +import React, {Fragment, useCallback, useEffect, useRef, useState} from 'react'; +import clsx from 'clsx'; +import {Button} from './Button'; +import {HexColorInput, HexColorPicker} from 'react-colorful'; +import {Tooltip} from './Tooltip'; +import {getAccentColor} from '../../utils/getAccentColor'; +import {useClickOutside} from '../../hooks/useClickOutside'; + +export function ColorPickerBeta({value, eyedropper, hasTransparentOption, onChange, children}) { + // HexColorInput doesn't support adding a ref on the input itself + const inputWrapperRef = useRef(null); + + const stopPropagation = useCallback((e) => { + e.stopPropagation(); + + const inputElement = inputWrapperRef.current?.querySelector('input'); + const isInputField = e.target === inputElement; + + // Allow text selection for events on the input field + if (isInputField) { + return; + } + + // Prevent closing the color picker when clicking somewhere inside it + inputWrapperRef.current?.querySelector('input')?.focus(); + + e.preventDefault(); + }, []); + + const isUsingColorPicker = useRef(false); + + const stopUsingColorPicker = useCallback(() => { + isUsingColorPicker.current = false; + inputWrapperRef.current?.querySelector('input')?.focus(); + + document.removeEventListener('mouseup', stopUsingColorPicker); + document.removeEventListener('touchend', stopUsingColorPicker); + }, []); + + const startUsingColorPicker = useCallback(() => { + isUsingColorPicker.current = true; + + document.addEventListener('mouseup', stopUsingColorPicker); + document.addEventListener('touchend', stopUsingColorPicker); + }, [stopUsingColorPicker]); + + const openColorPicker = useCallback((e) => { + e.preventDefault(); + + isUsingColorPicker.current = true; + document.body.style.setProperty('pointer-events', 'none'); + + const eyeDropper = new window.EyeDropper(); + eyeDropper.open() + .then(result => onChange(result.sRGBHex)) + .finally(() => { + isUsingColorPicker.current = false; + document.body.style.removeProperty('pointer-events'); + inputWrapperRef.current?.querySelector('input')?.focus(); + }); + }, [onChange]); + + useEffect(() => { + inputWrapperRef.current?.querySelector('input')?.focus(); + }, []); + + let hexValue = value; + if (value === 'accent') { + hexValue = getAccentColor(); + } else if (value === 'transparent') { + hexValue = ''; + } + + const focusHexInputOnClick = useCallback((e) => { + inputWrapperRef.current?.querySelector('input')?.focus(); + }, []); + + return ( +
    + +
    +
    + # + + {eyedropper && !!window.EyeDropper && ( + + )} +
    + + {hasTransparentOption &&
    +
    + ); +} + +function ColorSwatch({hex, accent, transparent, title, isSelected, onSelect}) { + const backgroundColor = accent ? getAccentColor() : hex; + + const ref = useRef(null); + + const onSelectHandler = (e) => { + e.preventDefault(); + + if (accent) { + onSelect('accent'); + } else if (transparent) { + onSelect('transparent'); + } else { + onSelect(hex); + } + }; + + return ( + + ); +} + +export function ColorIndicatorBeta({value, swatches, onSwatchChange, onTogglePicker, onChange, isExpanded, eyedropper, hasTransparentOption, children}) { + const [isOpen, setIsOpen] = useState(false); + const [showColorPicker, setShowColorPicker] = useState(false); + const [showChildren, setShowChildren] = useState(false); + const popoverRef = useRef(null); + + useClickOutside(isOpen, popoverRef, () => setIsOpen(false)); + + const stopPropagation = useCallback((e) => { + e.stopPropagation(); + e.preventDefault(); + }, []); + + useEffect(() => { + if (isExpanded) { + setIsOpen(true); + setShowChildren(true); + setShowColorPicker(false); + } + }, [isExpanded]); + + let backgroundColor = value; + let selectedSwatch = swatches.find(swatch => swatch.hex === value)?.title; + + if (value === 'accent') { + backgroundColor = getAccentColor(); + selectedSwatch = swatches.find(swatch => swatch.accent)?.title; + } else if (value === 'image') { + backgroundColor = 'transparent'; + selectedSwatch = swatches.find(swatch => swatch.image)?.title; + } else if (value === 'transparent') { + backgroundColor = 'white'; + selectedSwatch = swatches.find(swatch => swatch.transparent)?.title; + } + + if (isExpanded) { + selectedSwatch = null; + } + + const handleColorPickerChange = (newValue) => { + onChange(newValue); + // Don't close the popover when using the color picker + }; + + return ( +
    + + + {isOpen && ( +
    + {showColorPicker && ( + + )} + {showChildren && children} +
    +
    + {swatches.map(({customContent, ...swatch}) => ( + customContent ? + {customContent} : + { + onSwatchChange(val); + setShowColorPicker(false); + }} + {...swatch} + /> + ))} +
    + +
    +
    + )} +
    + ); +} diff --git a/packages/koenig-lexical/src/components/ui/ColorPickerBeta.stories.jsx b/packages/koenig-lexical/src/components/ui/ColorPickerBeta.stories.jsx new file mode 100644 index 0000000000..9ef599ecc2 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/ColorPickerBeta.stories.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {ColorPickerBeta} from './ColorPickerBeta'; + +const story = { + title: 'Generic/Color picker (beta)', + component: ColorPickerBeta, + parameters: { + status: { + type: 'uiReady' + } + }, + argTypes: { + selectedName: {control: 'select', options: ['grey', 'blue', 'green', 'yellow', 'red', 'pink', 'purple']} + } +}; +export default story; + +const Template = (args) => { + return ( +
    + +
    + ); +}; + +export const Default = Template.bind({}); +Default.args = { + swatches: [ + {title: 'Brand color', accent: true}, + {title: 'Black', hex: '#000000'}, + {title: 'Transparent', transparent: true} + ] +}; diff --git a/packages/koenig-lexical/src/components/ui/ImageUploadForm.jsx b/packages/koenig-lexical/src/components/ui/ImageUploadForm.jsx index 0b9806a467..b82c2a5ee9 100644 --- a/packages/koenig-lexical/src/components/ui/ImageUploadForm.jsx +++ b/packages/koenig-lexical/src/components/ui/ImageUploadForm.jsx @@ -11,6 +11,7 @@ export function ImageUploadForm({onFileChange, fileInputRef, mimeTypes = ['image multiple={multiple} name="image-input" type='file' + onClick={e => e.stopPropagation()} /> ); diff --git a/packages/koenig-lexical/src/components/ui/ImageUploadSwatch.jsx b/packages/koenig-lexical/src/components/ui/ImageUploadSwatch.jsx new file mode 100644 index 0000000000..114cb672ec --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/ImageUploadSwatch.jsx @@ -0,0 +1,25 @@ +import ImgBgIcon from '../../assets/icons/kg-img-bg.svg?react'; +import clsx from 'clsx'; +import {Tooltip} from './Tooltip'; + +export const ImageUploadSwatch = ({ + showBackgroundImage, + onClickHandler, + dataTestId +}) => { + return ( + + ); +}; diff --git a/packages/koenig-lexical/src/components/ui/MediaPlaceholderBeta.jsx b/packages/koenig-lexical/src/components/ui/MediaPlaceholderBeta.jsx new file mode 100644 index 0000000000..62bf4332c5 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/MediaPlaceholderBeta.jsx @@ -0,0 +1,148 @@ +import AudioPlaceholderIcon from '../../assets/icons/kg-audio-placeholder.svg?react'; +import FilePlaceholderIcon from '../../assets/icons/kg-file-placeholder.svg?react'; +import GalleryPlaceholderIcon from '../../assets/icons/kg-gallery-placeholder.svg?react'; +import ImgPlaceholderIcon from '../../assets/icons/kg-img-placeholder.svg?react'; +import ProductPlaceholderIcon from '../../assets/icons/kg-product-placeholder.svg?react'; +import PropTypes from 'prop-types'; +import React from 'react'; +import VideoPlaceholderIcon from '../../assets/icons/kg-video-placeholder.svg?react'; +import clsx from 'clsx'; + +export const PLACEHOLDER_ICONS = { + image: ImgPlaceholderIcon, + gallery: GalleryPlaceholderIcon, + video: VideoPlaceholderIcon, + audio: AudioPlaceholderIcon, + file: FilePlaceholderIcon, + product: ProductPlaceholderIcon +}; + +export const CardText = ({text, type}) => ( + + {text} + +); + +const ButtonContents = ({desc, hasErrors}) => { + if (hasErrors) { + return null; + } + return

    {desc}

    ; +}; + +const StandardContents = ({desc, hasErrors, icon, size}) => { + if (size === 'xsmall' && hasErrors) { + return null; + } + + const Icon = PLACEHOLDER_ICONS[icon]; + + const iconClasses = clsx( + 'shrink-0 opacity-80 transition-all ease-linear hover:scale-105 group-hover:opacity-100', + size === 'large' && 'size-20 text-grey', + size === 'small' && 'size-14 text-grey', + size === 'xsmall' && 'size-5 text-grey-700', + !['large', 'small', 'xsmall'].includes(size) && 'size-16 text-grey', + (size === 'xsmall' && desc) && 'mr-3' + ); + + const descriptionClasses = clsx( + 'flex min-w-[auto] !font-sans !text-sm !font-normal text-grey-700 opacity-80 transition-all group-hover:opacity-100', + size === 'xsmall' && '!mt-0', + size !== 'xsmall' && '!mt-4' + ); + + return <> + +

    {desc}

    + ; +}; + +export function MediaPlaceholderBeta({ + desc, + icon, + filePicker, + size, + type, + borderStyle = 'squared', + isDraggedOver, + errors = [], + placeholderRef, + dataTestId = 'media-placeholder', + errorDataTestId = 'media-placeholder-errors', + multiple = false, + ...props +}) { + const containerClasses = clsx( + 'relative flex h-full items-center justify-center', + type === 'button' ? 'rounded-lg bg-grey-100' : 'border bg-grey-50', + size === 'xsmall' && type !== 'button' && 'before:pb-[12.5%] dark:bg-grey-900', + size !== 'xsmall' && type !== 'button' && 'before:pb-[62.5%] dark:bg-grey-950', + borderStyle === 'rounded' && type !== 'button' && 'rounded-lg border-grey/20 dark:border-transparent', + borderStyle !== 'rounded' && type !== 'button' && 'border-grey/20 dark:border-grey/10' + ); + + const buttonClasses = clsx( + 'group flex cursor-pointer select-none items-center justify-center', + type === 'button' && 'px-3 py-1', + type !== 'button' && (size === 'xsmall' ? 'p-4' : 'flex-col p-20') + ); + + const errorClasses = clsx( + 'font-sans text-sm font-semibold text-red', + size !== 'xsmall' && 'mt-3 max-w-[65%]' + ); + + const errorMessages = errors.map(error => ( + + {error.message} + + )); + + return ( +
    +
    + {isDraggedOver ? ( + + ) : ( + + )} +
    +
    + ); +} + +MediaPlaceholderBeta.propTypes = { + icon: PropTypes.oneOf(['image', 'gallery', 'video', 'audio', 'file', 'product']), + desc: PropTypes.string, + size: PropTypes.oneOf(['xsmall', 'small', 'medium', 'large']), + type: PropTypes.oneOf(['image', 'button']), + borderStyle: PropTypes.oneOf(['squared', 'rounded']) +}; diff --git a/packages/koenig-lexical/src/components/ui/MediaPlaceholderBeta.stories.jsx b/packages/koenig-lexical/src/components/ui/MediaPlaceholderBeta.stories.jsx new file mode 100644 index 0000000000..3cb3163406 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/MediaPlaceholderBeta.stories.jsx @@ -0,0 +1,91 @@ +import React from 'react'; + +import {MediaPlaceholderBeta} from './MediaPlaceholderBeta'; + +const story = { + title: 'Generic/Media placeholder (beta)', + component: MediaPlaceholderBeta, + argTypes: { + icon: { + options: ['image', 'gallery', 'video', 'audio', 'file', 'product'], + control: {type: 'select'} + }, + size: { + options: ['xsmall', 'small', 'medium', 'large'], + control: {type: 'select'} + }, + borderStyle: { + options: ['squared', 'rounded'], + control: {type: 'radio'} + } + }, + parameters: { + status: { + type: 'functional' + } + } +}; +export default story; + +const Template = args => ( +
    + +
    +); + +export const Image = Template.bind({}); +Image.args = { + icon: 'image', + desc: 'Click to select an image', + size: 'medium', + borderStyle: 'squared' +}; + +export const Gallery = Template.bind({}); +Gallery.args = { + icon: 'gallery', + desc: 'Click to select up to 9 images', + size: 'large', + borderStyle: 'squared' +}; + +export const Video = Template.bind({}); +Video.args = { + icon: 'video', + desc: 'Click to select a video', + size: 'medium', + borderStyle: 'squared' +}; + +export const Audio = Template.bind({}); +Audio.args = { + icon: 'audio', + desc: 'Click to upload an audio file', + size: 'xsmall', + borderStyle: 'squared' +}; + +export const File = Template.bind({}); +File.args = { + icon: 'file', + desc: 'Click to upload a file', + size: 'xsmall', + borderStyle: 'squared' +}; + +export const Product = Template.bind({}); +Product.args = { + icon: 'product', + desc: 'Click to upload a product image', + size: 'small', + borderStyle: 'squared' +}; + +export const ErrorState = Template.bind({}); +ErrorState.args = { + icon: 'video', + desc: 'Click to select a video', + size: 'medium', + borderStyle: 'squared', + errors: [{message: 'The file type you uploaded is not supported. Please use .MP4, .WEBM, .OGV'}] +}; diff --git a/packages/koenig-lexical/src/components/ui/MediaUploaderBeta.jsx b/packages/koenig-lexical/src/components/ui/MediaUploaderBeta.jsx new file mode 100644 index 0000000000..4b25343776 --- /dev/null +++ b/packages/koenig-lexical/src/components/ui/MediaUploaderBeta.jsx @@ -0,0 +1,141 @@ +import DeleteIcon from '../../assets/icons/kg-trash.svg?react'; +import ImageUploadForm from './ImageUploadForm'; +import PropTypes from 'prop-types'; +import WandIcon from '../../assets/icons/kg-wand.svg?react'; +import clsx from 'clsx'; +import {IconButton} from './IconButton'; +import {MediaPlaceholderBeta} from './MediaPlaceholderBeta'; +import {ProgressBar} from './ProgressBar'; +import {openFileSelection} from '../../utils/openFileSelection'; +import {useRef} from 'react'; + +export function MediaUploaderBeta({ + className, + imgClassName, + src, + alt, + desc, + icon, + size, + type, + borderStyle = 'squared', + backgroundSize = 'cover', + mimeTypes, + onFileChange, + dragHandler, + isEditing = true, + isLoading, + isPinturaEnabled, + openImageEditor, + progress, + errors, + onRemoveMedia = () => {}, + additionalActions, + setFileInputRef +}) { + const fileInputRef = useRef(null); + + const onFileInputRef = (element) => { + fileInputRef.current = element; + setFileInputRef?.(element); + }; + + const progressStyle = { + width: `${progress?.toFixed(0)}%` + }; + + const onRemove = (e) => { + e.stopPropagation(); // prevents card from losing selected state + onRemoveMedia(); + }; + + const isEmpty = !isLoading && !src; + + if (isEmpty) { + return ( +
    + openFileSelection({fileInputRef})} + icon={icon} + isDraggedOver={dragHandler?.isDraggedOver} + placeholderRef={dragHandler?.setRef} + size={size} + type={type} + /> + openFileSelection({fileInputRef})} + mimeTypes={mimeTypes} + onFileChange={onFileChange} + /> +
    + ); + } + + return ( +
    + {src && ( + <> + {alt} +
    + + )} + + {!isLoading && ( +
    + {additionalActions} + { isPinturaEnabled && openImageEditor({ + image: src, + handleSave: (editedImage) => { + onFileChange({ + target: { + files: [editedImage] + } + }); + } + })} /> } + +
    + )} + + {isLoading && ( +
    + +
    + )} +
    + ); +} + +MediaUploaderBeta.propTypes = { + additionalActions: PropTypes.node, + alt: PropTypes.string, + backgroundSize: PropTypes.oneOf(['cover', 'contain']), + borderStyle: PropTypes.oneOf(['squared', 'rounded']), + className: PropTypes.string, + desc: PropTypes.string, + dragHandler: PropTypes.shape({isDraggedOver: PropTypes.bool, setRef: PropTypes.func}), + errors: PropTypes.arrayOf(PropTypes.shape({message: PropTypes.string})), + icon: PropTypes.string, + imgClassName: PropTypes.string, + isEditing: PropTypes.bool, + isLoading: PropTypes.bool, + isPinturaEnabled: PropTypes.bool, + mimeTypes: PropTypes.arrayOf(PropTypes.string), + onFileChange: PropTypes.func, + onRemoveMedia: PropTypes.func, + openImageEditor: PropTypes.func, + progress: PropTypes.number, + setFileInputRef: PropTypes.func, + size: PropTypes.string, + src: PropTypes.string, + type: PropTypes.oneOf(['image', 'button']) +}; diff --git a/packages/koenig-lexical/src/components/ui/SettingsPanel.jsx b/packages/koenig-lexical/src/components/ui/SettingsPanel.jsx index ef1b357724..4d99536a60 100644 --- a/packages/koenig-lexical/src/components/ui/SettingsPanel.jsx +++ b/packages/koenig-lexical/src/components/ui/SettingsPanel.jsx @@ -3,12 +3,16 @@ import React from 'react'; import clsx from 'clsx'; import useSettingsPanelReposition from '../../hooks/useSettingsPanelReposition'; import {ButtonGroup} from './ButtonGroup'; +import {ButtonGroupBeta} from './ButtonGroupBeta'; import {ColorIndicator, ColorPicker} from './ColorPicker'; +import {ColorIndicatorBeta} from './ColorPickerBeta'; import {ColorOptionButtons} from './ColorOptionButtons'; +import {ColorOptionButtonsBeta} from './ColorOptionButtonsBeta.jsx'; import {Dropdown} from './Dropdown'; import {Input} from './Input'; import {InputList, InputListItem} from './InputList.jsx'; import {MediaUploader} from './MediaUploader'; +import {MediaUploaderBeta} from './MediaUploaderBeta'; import {MultiSelectDropdown} from './MultiSelectDropdown'; import {Slider} from './Slider.jsx'; import {TabView} from './TabView'; @@ -231,6 +235,18 @@ export function ButtonGroupSetting({label, onClick, selectedName, buttons}) { ); } +export function ButtonGroupSettingBeta({label, onClick, selectedName, buttons, hasTooltip}) { + return ( +
    +
    {label}
    + +
    + +
    +
    + ); +} + export function ColorOptionSetting({label, onClick, selectedName, buttons, layout, dataTestId}) { return (
    @@ -243,6 +259,18 @@ export function ColorOptionSetting({label, onClick, selectedName, buttons, layou ); } +export function ColorOptionSettingBeta({label, onClick, selectedName, buttons, layout, dataTestId}) { + return ( +
    +
    {label}
    + +
    + +
    +
    + ); +} + export function ColorPickerSetting({label, isExpanded, onSwatchChange, onPickerChange, onTogglePicker, value, swatches, eyedropper, hasTransparentOption, dataTestId}) { const mappedPicker = (event) => { onTogglePicker(true); @@ -286,15 +314,81 @@ export function ColorPickerSetting({label, isExpanded, onSwatchChange, onPickerC ); } -export function MediaUploadSetting({className, label, hideLabel, onFileChange, isDraggedOver, placeholderRef, src, alt, isLoading, errors = [], progress, onRemoveMedia, icon, desc = '', size, stacked, borderStyle, mimeTypes, isPinturaEnabled, openImageEditor, setFileInputRef}) { +export function ColorPickerSettingBeta({label, isExpanded, onSwatchChange, onPickerChange, onTogglePicker, value, swatches, eyedropper, hasTransparentOption, dataTestId, customToolbarContent, children}) { + const markClickedInside = (event) => { + event.stopPropagation(); + }; + return ( -
    -
    {label}
    +
    +
    +
    {label}
    + +
    + + {children} + +
    +
    +
    + ); +} + +export function MediaUploadSetting({className, label, hideLabel, onFileChange, isDraggedOver, placeholderRef, src, alt, isLoading, errors = [], progress, onRemoveMedia, icon, desc, size, type, stacked, borderStyle, mimeTypes, isPinturaEnabled, openImageEditor, setFileInputRef}) { + return ( +
    +
    {label}
    +
    + ); +} + +export function MediaUploadSettingBeta({className, label, hideLabel, onFileChange, isDraggedOver, placeholderRef, src, alt, isLoading, errors = [], progress, onRemoveMedia, icon, desc, size, type, stacked, borderStyle, mimeTypes, isPinturaEnabled, openImageEditor, setFileInputRef}) { + return ( +
    +
    {label}
    + + diff --git a/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.jsx b/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.jsx index 7efae0942e..e37c5897ec 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CallToActionCard.jsx @@ -7,7 +7,7 @@ import ReplacementStringsPlugin from '../../../plugins/ReplacementStringsPlugin. import clsx from 'clsx'; import defaultTheme from '../../../themes/default.js'; import {Button} from '../Button.jsx'; -import {ButtonGroupSetting, ColorOptionSetting, ColorPickerSetting, InputSetting, InputUrlSetting, MediaUploadSetting, SettingsPanel, ToggleSetting} from '../SettingsPanel.jsx'; +import {ButtonGroupSettingBeta, ColorOptionSettingBeta, ColorPickerSettingBeta, InputSetting, InputUrlSetting, MediaUploadSettingBeta, SettingsPanel, ToggleSetting} from '../SettingsPanel.jsx'; import {ReadOnlyOverlay} from '../ReadOnlyOverlay.jsx'; import {RestrictContentPlugin} from '../../../index.js'; import {VisibilitySettings} from '../VisibilitySettings.jsx'; @@ -21,7 +21,9 @@ export const CALLTOACTION_COLORS = { blue: 'bg-blue/10 border-transparent', green: 'bg-green/10 border-transparent', yellow: 'bg-yellow/10 border-transparent', - red: 'bg-red/10 border-transparent' + red: 'bg-red/10 border-transparent', + pink: 'bg-pink/10 border-transparent', + purple: 'bg-purple/10 border-transparent' }; const sponsoredLabelTheme = { @@ -43,27 +45,37 @@ export const callToActionColorPicker = [ { label: 'Grey', name: 'grey', - color: 'bg-grey/15 border-black/[.08] dark:border-white/10' + color: 'bg-grey/20 border-black/[.08] dark:border-white/10' }, { label: 'Blue', name: 'blue', - color: 'bg-blue/15 border-black/[.08] dark:border-white/10' + color: 'bg-blue/20 border-black/[.08] dark:border-white/10' }, { label: 'Green', name: 'green', - color: 'bg-green/15 border-black/[.08] dark:border-white/10' + color: 'bg-green/20 border-black/[.08] dark:border-white/10' }, { label: 'Yellow', name: 'yellow', - color: 'bg-yellow/15 border-black/[.08] dark:border-white/10' + color: 'bg-yellow/20 border-black/[.08] dark:border-white/10' }, { label: 'Red', name: 'red', - color: 'bg-red/15 border-black/[.08] dark:border-white/10' + color: 'bg-red/20 border-black/[.08] dark:border-white/10' + }, + { + label: 'Pink', + name: 'pink', + color: 'bg-pink/20 border-black/[0.08] dark:border-white/10' + }, + { + label: 'Purple', + name: 'purple', + color: 'bg-purple/20 border-black/[0.08] dark:border-white/10' } ]; @@ -93,7 +105,8 @@ export function CallToActionCard({ updateHasSponsorLabel = () => {}, updateLayout = () => {}, updateShowButton = () => {}, - toggleVisibility = () => {} + toggleVisibility = () => {}, + imageDragHandler = {} }) { const [buttonColorPickerExpanded, setButtonColorPickerExpanded] = useState(false); @@ -107,13 +120,15 @@ export function CallToActionCard({ label: 'Minimal', name: 'minimal', Icon: MinimalLayoutIcon, - dataTestId: 'minimal-layout' + dataTestId: 'minimal-layout', + ariaLabel: 'Left-aligned layout with small, square image' }, { label: 'Immersive', name: 'immersive', Icon: ImmersiveLayoutIcon, - dataTestId: 'immersive-layout' + dataTestId: 'immersive-layout', + ariaLabel: 'Center-aligned layout with full-width image and button' } ]; @@ -123,20 +138,22 @@ export function CallToActionCard({ const designSettings = ( <> - {/* Color picker */} - {/* Layout settings */} - + {/* Color picker */} + {/* Sponsor label setting */} {/* Image setting */} - +
    {/* Button settings */} {showButton && ( <> - @@ -264,6 +286,7 @@ export function CallToActionCard({ layout === 'immersive' ? 'h-auto w-full' : 'aspect-square w-16 object-cover', 'rounded-md' )} + data-testid="cta-card-image" src={imageSrc} />
    @@ -280,7 +303,7 @@ export function CallToActionCard({ placeholderClassName={`bg-transparent whitespace-normal font-serif text-xl !text-grey-500 !dark:text-grey-800 ` } placeholderText="Write something worth clicking..." textClassName={clsx( - 'w-full whitespace-normal text-pretty bg-transparent font-serif text-xl text-grey-900 dark:text-grey-200', + 'koenig-lexical-cta-text w-full whitespace-normal text-pretty bg-transparent font-serif text-xl text-grey-900 dark:text-grey-200', layout === 'immersive' ? 'text-center' : 'text-left' )} > @@ -295,8 +318,8 @@ export function CallToActionCard({ dataTestId="cta-button" placeholder="Add button text" size={layout === 'immersive' ? 'medium' : 'small'} - style={buttonColor ? { - backgroundColor: buttonColor === 'accent' ? 'var(--accent-color)' : buttonColor, + style={buttonColor !== 'accent' ? { + backgroundColor: buttonColor, color: buttonTextColor } : undefined} value={buttonText} @@ -332,7 +355,7 @@ CallToActionCard.propTypes = { buttonUrl: PropTypes.string, buttonColor: PropTypes.string, buttonTextColor: PropTypes.string, - color: PropTypes.oneOf(['none', 'grey', 'white', 'blue', 'green', 'yellow', 'red']), + color: PropTypes.oneOf(['none', 'grey', 'white', 'blue', 'green', 'yellow', 'red', 'pink', 'purple']), hasSponsorLabel: PropTypes.bool, imageSrc: PropTypes.string, isEditing: PropTypes.bool, @@ -352,6 +375,8 @@ CallToActionCard.propTypes = { onRemoveMedia: PropTypes.func, sponsorLabelHtmlEditor: PropTypes.object, sponsorLabelHtmlEditorInitialState: PropTypes.object, - visibilityOptions: PropTypes.object, - toggleVisibility: PropTypes.func + visibilityOptions: PropTypes.array, + toggleVisibility: PropTypes.func, + imageUploadHandler: PropTypes.func, + imageDragHandler: PropTypes.object }; diff --git a/packages/koenig-lexical/src/components/ui/cards/CalloutCard.jsx b/packages/koenig-lexical/src/components/ui/cards/CalloutCard.jsx index 654e22c839..f40bb1c50f 100644 --- a/packages/koenig-lexical/src/components/ui/cards/CalloutCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/CalloutCard.jsx @@ -43,37 +43,37 @@ export const calloutColorPicker = [ { label: 'Grey', name: 'grey', - color: 'bg-grey/15 border-black/[0.08] dark:border-white/10' + color: 'bg-grey/20 border-black/[0.08] dark:border-white/10' }, { label: 'Blue', name: 'blue', - color: 'bg-blue/15 border-black/[0.08] dark:border-white/10' + color: 'bg-blue/20 border-black/[0.08] dark:border-white/10' }, { label: 'Green', name: 'green', - color: 'bg-green/15 border-black/[0.08] dark:border-white/10' + color: 'bg-green/20 border-black/[0.08] dark:border-white/10' }, { label: 'Yellow', name: 'yellow', - color: 'bg-yellow/15 border-black/[0.08] dark:border-white/10' + color: 'bg-yellow/20 border-black/[0.08] dark:border-white/10' }, { label: 'Red', name: 'red', - color: 'bg-red/15 border-black/[0.08] dark:border-white/10' + color: 'bg-red/20 border-black/[0.08] dark:border-white/10' }, { label: 'Pink', name: 'pink', - color: 'bg-pink/15 border-black/[0.08] dark:border-white/10' + color: 'bg-pink/20 border-black/[0.08] dark:border-white/10' }, { label: 'Purple', name: 'purple', - color: 'bg-purple/15 border-black/[0.08] dark:border-white/10' + color: 'bg-purple/20 border-black/[0.08] dark:border-white/10' }, { label: 'Accent', diff --git a/packages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.jsx b/packages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.jsx index fae3396026..0b6c7da56f 100644 --- a/packages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.jsx +++ b/packages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.jsx @@ -178,6 +178,7 @@ export function HeaderCard({isEditing, /> { + if (!enabled) { + return; + } + + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + handler(); + } + }; + + window.addEventListener('mousedown', handleClickOutside, {capture: true}); + return () => window.removeEventListener('mousedown', handleClickOutside, {capture: true}); + }, [enabled, handler, ref]); +} diff --git a/packages/koenig-lexical/src/hooks/useMovable.js b/packages/koenig-lexical/src/hooks/useMovable.js index 720246ebfe..b13149b592 100644 --- a/packages/koenig-lexical/src/hooks/useMovable.js +++ b/packages/koenig-lexical/src/hooks/useMovable.js @@ -125,12 +125,16 @@ export default function useMovable({adjustOnResize, adjustOnDrag} = {}) { // preventing clicks stops any event handlers that may otherwise result in the // movable element being closed when the drag finishes const disablePointerEvents = useCallback(() => { - ref.current.style.pointerEvents = 'none'; + if (ref.current) { + ref.current.style.pointerEvents = 'none'; + } window.addEventListener('click', cancelClick, {capture: true, passive: false}); }, [ref, cancelClick]); const enablePointerEvents = useCallback(() => { - ref.current.style.pointerEvents = ''; + if (ref.current) { + ref.current.style.pointerEvents = ''; + } window.removeEventListener('click', cancelClick, {capture: true, passive: false}); }, [ref, cancelClick]); diff --git a/packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx b/packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx index 7b6050b199..1dfb890985 100644 --- a/packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx +++ b/packages/koenig-lexical/src/nodes/CallToActionNodeComponent.jsx @@ -1,6 +1,7 @@ import CardContext from '../context/CardContext'; import KoenigComposerContext from '../context/KoenigComposerContext.jsx'; import React, {useRef} from 'react'; +import useFileDragAndDrop from '../hooks/useFileDragAndDrop'; import {$getNodeByKey} from 'lexical'; import {ActionToolbar} from '../components/ui/ActionToolbar.jsx'; import {CallToActionCard} from '../components/ui/cards/CallToActionCard.jsx'; @@ -24,7 +25,6 @@ export const CallToActionNodeComponent = ({ htmlEditor, htmlEditorInitialState, buttonTextColor, - href, sponsorLabelHtmlEditor, sponsorLabelHtmlEditorInitialState }) => { @@ -32,6 +32,7 @@ export const CallToActionNodeComponent = ({ const {isEditing, isSelected, setEditing} = React.useContext(CardContext); const {fileUploader, cardConfig} = React.useContext(KoenigComposerContext); const [showSnippetToolbar, setShowSnippetToolbar] = React.useState(false); + const imageDragHandler = useFileDragAndDrop({handleDrop: handleImageDrop}); const {visibilityOptions, toggleVisibility} = useVisibilityToggle(editor, nodeKey, cardConfig); @@ -124,6 +125,10 @@ export const CallToActionNodeComponent = ({ }); }; + async function handleImageDrop(files) { + await handleImageChange(files); + } + React.useEffect(() => { htmlEditor.setEditable(isEditing); }, [isEditing, htmlEditor]); @@ -141,6 +146,7 @@ export const CallToActionNodeComponent = ({ hasSponsorLabel={hasSponsorLabel} htmlEditor={htmlEditor} htmlEditorInitialState={htmlEditorInitialState} + imageDragHandler={imageDragHandler} imageSrc={imageUrl} imageUploader={imageUploader} isEditing={isEditing} diff --git a/packages/koenig-lexical/src/styles/components/kg-prose.css b/packages/koenig-lexical/src/styles/components/kg-prose.css index d183aad1d3..b7f945150c 100644 --- a/packages/koenig-lexical/src/styles/components/kg-prose.css +++ b/packages/koenig-lexical/src/styles/components/kg-prose.css @@ -981,6 +981,14 @@ } } +.koenig-lexical-cta-label a { + @apply !text-grey-900 dark:!text-grey-200 underline; +} + +.koenig-lexical-cta-text a { + @apply !text-grey-900 dark:!text-grey-200; +} + /* stylelint-enable at-rule-disallowed-list */ diff --git a/packages/koenig-lexical/tailwind.config.cjs b/packages/koenig-lexical/tailwind.config.cjs index f6133fb4fe..925d003c2e 100644 --- a/packages/koenig-lexical/tailwind.config.cjs +++ b/packages/koenig-lexical/tailwind.config.cjs @@ -91,6 +91,7 @@ module.exports = { }, boxShadow: { DEFAULT: '0 0 1px rgba(0,0,0,.15), 0px 13px 27px -5px rgba(50, 50, 93, 0.08), 0px 8px 16px -8px rgba(0, 0, 0, 0.12)', + xs: '0px 1px 2px rgba(0, 0, 0, 0.06)', sm: '0px 2px 5px -1px rgba(50, 50, 93, 0.2), 0px 1px 3px -1px rgba(0, 0, 0, 0.25)', md: '0px 13px 27px -5px rgba(50, 50, 93, 0.25), 0px 8px 16px -8px rgba(0, 0, 0, 0.3)', lg: '0px 50px 100px -25px rgba(50, 50, 93, 0.2), 0px 30px 60px -20px rgba(0, 0, 0, 0.25)', diff --git a/packages/koenig-lexical/test/e2e/cards/cta-card.test.js b/packages/koenig-lexical/test/e2e/cards/call-to-action-card.test.js similarity index 87% rename from packages/koenig-lexical/test/e2e/cards/cta-card.test.js rename to packages/koenig-lexical/test/e2e/cards/call-to-action-card.test.js index 26e05e34a0..a3da9151c0 100644 --- a/packages/koenig-lexical/test/e2e/cards/cta-card.test.js +++ b/packages/koenig-lexical/test/e2e/cards/call-to-action-card.test.js @@ -1,5 +1,6 @@ import path from 'path'; -import {assertHTML, focusEditor, getEditorStateJSON, html, initialize, insertCard} from '../../utils/e2e'; +import {assertHTML, createDataTransfer, focusEditor, getEditorStateJSON, html, initialize, insertCard} from '../../utils/e2e'; +import {cardBackgroundColorSettings} from '../../utils/background-color-helper'; import {expect, test} from '@playwright/test'; import {fileURLToPath} from 'url'; const __filename = fileURLToPath(import.meta.url); @@ -118,37 +119,30 @@ test.describe('Call To Action Card', async () => { `, {ignoreCardContents: true}); }); - test('can toggle button on card', async function () { + test('button and button settings is visible by default', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); expect(await page.isVisible('[data-testid="cta-button"]')).toBe(true); - - await page.click('[data-testid="button-settings"]'); - - expect(await page.isVisible('[data-testid="cta-button"]')).toBe(false); + expect(await page.isVisible('[data-testid="cta-button-color"]')).toBe(true); + expect(await page.isVisible('[data-testid="button-text"]')).toBe(true); + expect(await page.isVisible('[data-testid="button-url"]')).toBe(true); }); - test('button settings expands and collapses when toggled', async function () { + test('can toggle button on card and expands settings', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); + expect(await page.isVisible('[data-testid="cta-button"]')).toBe(true); await page.click('[data-testid="button-settings"]'); - // determine if settings are open byy looking for cta-button-color, button-text & button-url - expect(await page.isVisible('[data-testid="cta-button-color"]')).toBe(true); - expect(await page.isVisible('[data-testid="button-text"]')).toBe(true); - expect(await page.isVisible('[data-testid="button-url"]')).toBe(true); + expect(await page.isVisible('[data-testid="cta-button"]')).toBe(false); await page.click('[data-testid="button-settings"]'); - // determine if settings are closed by looking for cta-button-color, button-text & button-url - expect(await page.isVisible('[data-testid="cta-button-color"]')).toBe(false); - expect(await page.isVisible('[data-testid="button-text"]')).toBe(false); - expect(await page.isVisible('[data-testid="button-url"]')).toBe(false); + + expect(await page.isVisible('[data-testid="cta-button"]')).toBe(true); }); test('can set button text', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); await page.fill('[data-testid="button-text"]', 'Click me'); expect(await page.textContent('[data-testid="cta-button"]')).toBe('Click me'); }); @@ -156,7 +150,6 @@ test.describe('Call To Action Card', async () => { test('can set button url', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); await page.fill('[data-testid="button-url"]', 'https://example.com/somepost'); const buttonContainer = await page.$('[data-test-cta-button-current-url]'); const currentUrl = await buttonContainer.getAttribute('data-test-cta-button-current-url'); @@ -167,7 +160,6 @@ test.describe('Call To Action Card', async () => { test('suggested urls display', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); const buttonTextInput = await page.getByTestId('button-url'); await expect(buttonTextInput).toHaveValue(''); @@ -188,7 +180,6 @@ test.describe('Call To Action Card', async () => { test('button doesnt disappear when toggled, has text, has url and loses focus', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); await page.fill('[data-testid="button-text"]', 'Click me'); await page.fill('[data-testid="button-url"]', 'https://example.com/somepost'); expect(await page.isVisible('[data-testid="cta-button"]')).toBe(true); @@ -217,16 +208,15 @@ test.describe('Call To Action Card', async () => { test('default button colour is accent', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); expect(await page.getAttribute('[data-testid="cta-button"]', 'class')).toContain('bg-accent'); }); test('can change button colour to black', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); // find the parent element cta-button-color and select child button with title=black - await page.click('[data-testid="cta-button-color"] button[title="Black"]'); + // await page.click('[data-testid="cta-button-color"] button[title="Black"]'); + await cardBackgroundColorSettings(page, {cardColorPickerTestId: 'cta-button-color', findByColorTitle: 'Black'}); // check if the button has style="background-color: rgb(0, 0, 0);" expect(await page.getAttribute('[data-testid="cta-button"]', 'style')).toContain('background-color: rgb(0, 0, 0);'); }); @@ -234,9 +224,8 @@ test.describe('Call To Action Card', async () => { test('can change button colour to grey', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); // find the parent element cta-button-color and select child button with title=white - await page.click('[data-testid="cta-button-color"] button[title="Grey"]'); + await cardBackgroundColorSettings(page, {cardColorPickerTestId: 'cta-button-color', findByColorTitle: 'Grey'}); // check if the button has style="background-color: rgb(255, 255, 255);" expect(await page.getAttribute('[data-testid="cta-button"]', 'style')).toContain('background-color: rgb(240, 240, 240);'); }); @@ -244,19 +233,16 @@ test.describe('Call To Action Card', async () => { test('can use colour picker to change button colour', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); - await page.click('button[aria-label="Pick color"]'); - await page.fill('input[aria-label="Color value"]', 'ff0000'); + await cardBackgroundColorSettings(page, {cardColorPickerTestId: 'cta-button-color', customColor: 'ff0000'}); expect(await page.getAttribute('[data-testid="cta-button"]', 'style')).toContain('background-color: rgb(255, 0, 0);'); }); test('button text colour changes with button colour', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); - await page.click('[data-testid="button-settings"]'); await page.fill('[data-testid="button-text"]', 'Click me'); - await page.click('button[aria-label="Pick color"]'); - await page.fill('input[aria-label="Color value"]', 'FFFFFF'); + + await cardBackgroundColorSettings(page, {cardColorPickerTestId: 'cta-button-color', customColor: 'FFFFFF'}); expect(await page.getAttribute('[data-testid="cta-button"]', 'style')).toContain('color: rgb(255, 255, 255);'); // change button colour to black @@ -326,19 +312,38 @@ test.describe('Call To Action Card', async () => { {testId: 'color-picker-green', expectedClass: 'bg-green'}, {testId: 'color-picker-blue', expectedClass: 'bg-blue'}, {testId: 'color-picker-yellow', expectedClass: 'bg-yellow'}, - {testId: 'color-picker-red', expectedClass: 'bg-red'} + {testId: 'color-picker-red', expectedClass: 'bg-red'}, + {testId: 'color-picker-pink', expectedClass: 'bg-pink'}, + {testId: 'color-picker-purple', expectedClass: 'bg-purple'} ]; await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); const firstChildSelector = '[data-kg-card="call-to-action"] > :first-child'; - await expect(page.locator(firstChildSelector)).not.toHaveClass(/bg-(green|blue|yellow|red)/); // shouldn't have any of the classes yet + await expect(page.locator(firstChildSelector)).not.toHaveClass(/bg-(green|blue|yellow|red|pink|purple)/); // shouldn't have any of the classes yet for (const color of colors) { - await page.click(`[data-test-id="${color.testId}"]`); + await page.locator('[data-testid="cta-background-color-picker"] button').click(); + await page.locator(`[data-test-id="${color.testId}"]`).click(); await expect(page.locator(firstChildSelector)).toHaveClass(new RegExp(color.expectedClass)); } }); + test('background color popup closes on outside click', async function () { + await focusEditor(page); + await insertCard(page, {cardName: 'call-to-action'}); + + const colorOptions = page.getByTestId('cta-background-color-picker'); + await colorOptions.getByTestId('color-options-button').click(); + + await expect(colorOptions.getByTestId('color-options-popover')).toBeVisible(); + + const card = page.locator('[data-kg-card="call-to-action"]'); + const settings = card.getByTestId('settings-panel'); + await settings.getByTestId('media-upload-setting').click(); + + await expect(colorOptions.getByTestId('color-options-popover')).not.toBeVisible(); + }); + test('can add and remove CTA Card image', async function () { const filePath = path.relative(process.cwd(), __dirname + `/../fixtures/large-image.jpeg`); @@ -359,6 +364,23 @@ test.describe('Call To Action Card', async () => { await expect(imgLocator).not.toBeVisible(); }); + test('can drag and drop image over upload button', async function () { + const filePath = path.relative(process.cwd(), __dirname + '/../fixtures/large-image.png'); + await focusEditor(page); + await insertCard(page, {cardName: 'call-to-action'}); + + // Create and dispatch data transfer + const dataTransfer = await createDataTransfer(page, [{filePath, fileName: 'large-image.png', fileType: 'image/png'}]); + await page.getByTestId('media-upload-placeholder').dispatchEvent('dragover', {dataTransfer}); + // Dragover text should be visible + // check that "Drop it like it's hot" is visible + await expect(await page.locator('[data-kg-card-drag-text="true"]')).toBeVisible(); + + await page.getByTestId('media-upload-placeholder').dispatchEvent('drop', {dataTransfer}); + + await expect (await page.getByTestId('cta-card-image')).toBeVisible(); + }); + test('default layout is minimal', async function () { await focusEditor(page); await insertCard(page, {cardName: 'call-to-action'}); diff --git a/packages/koenig-lexical/test/e2e/cards/gallery-card.test.js b/packages/koenig-lexical/test/e2e/cards/gallery-card.test.js index 50d610f135..9c14befb78 100644 --- a/packages/koenig-lexical/test/e2e/cards/gallery-card.test.js +++ b/packages/koenig-lexical/test/e2e/cards/gallery-card.test.js @@ -507,7 +507,8 @@ test.describe('Gallery card', async () => { const fileChooser = await fileChooserPromise; await fileChooser.setFiles(filePaths); - await expect(page.locator('[data-testid="gallery-image"]')).toHaveCount(9); + await expect(page.getByTestId('progress-bar')).not.toBeVisible(); + await expect(page.getByTestId('gallery-image')).toHaveCount(9); }); const editorState = await getEditorState(page); diff --git a/packages/koenig-lexical/test/utils/background-color-helper.js b/packages/koenig-lexical/test/utils/background-color-helper.js new file mode 100644 index 0000000000..9f7cd0b5d7 --- /dev/null +++ b/packages/koenig-lexical/test/utils/background-color-helper.js @@ -0,0 +1,28 @@ +export async function cardBackgroundColorSettings(page, {cardColorPickerTestId, customColor, colorTestId, findByColorTitle, imageUploadId, fireColorSetting = true}) { + if (fireColorSetting) { + const colorSetting = page.locator(`[data-testid="${cardColorPickerTestId}"]`); + const colorButton = colorSetting.locator('button'); + await colorButton.click(); + } + + if (findByColorTitle) { + const colorTitle = page.locator(`[title="${findByColorTitle}"]`); + await colorTitle.click(); + } + + if (customColor) { + const picker = page.locator(`[data-testid="color-picker-toggle"]`); + await picker.click(); + const colorInput = page.locator(`input[aria-label="Color value"]`); + await colorInput.click({clickCount: 3}); + await colorInput.type(customColor); + } + + if (colorTestId) { + await page.locator(`[data-test-id="${colorTestId}"]`).click(); + } + + if (imageUploadId) { + await page.locator(`[data-testid="${imageUploadId}"]`).click(); + } +}
    - + ${dataset.buttonText}