Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[DEV-11237] Implement CTA button support in editor #456

Merged
merged 11 commits into from
Aug 10, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface RenderProps {
export interface Props
extends Omit<RenderElementProps, 'attributes' | 'children'>,
SlateInternalAttributes {
align?: Alignment;
align?: `${Alignment}`;
border?: boolean;
className?: string;
element: ElementNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
&.variant-icon-label {
opacity: 0.5;
padding-top: $spacing-half;
min-width: 74px;
border-radius: 8px;

.hidden-input:not(:disabled) + &:hover {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function OptionsGroup<T extends string>(props: OptionsGroupProps<T>) {
value={o.value}
icon={o.icon}
disabled={o.disabled ?? props.disabled}
variantClassName={variantClassName}
variantClassName={classNames(variantClassName)}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ export namespace SearchInput {
onSelect: (suggestion: Suggestion<T>) => void;
}

export namespace Props {
// Note: using `declare` here to not confuse Babel. @see https://babeljs.io/docs/babel-plugin-transform-typescript#impartial-namespace-support
export declare namespace Props {
export interface Empty {
query: string;
loading: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createDeserializeElement, type Extension } from '@prezly/slate-commons';
import React from 'react';
import type { RenderElementProps } from 'slate-react';

import { composeElementDeserializer } from '#modules/html-deserialization';

import { ButtonBlockNode } from './ButtonBlockNode';
import { ButtonBlockElement } from './components';
import {
fixDuplicateButtonBlockUuid,
normalizeRedundantButtonBlockAttributes,
parseSerializedButtonBlockElement,
} from './lib';

export const EXTENSION_ID = 'ButtonBlockExtension';

export interface ButtonBlockExtensionConfiguration {
withNewTabOption?: boolean;
}

export function ButtonBlockExtension({
withNewTabOption = true,
}: ButtonBlockExtensionConfiguration): Extension {
return {
id: EXTENSION_ID,
deserialize: {
element: composeElementDeserializer({
[ButtonBlockNode.Type]: createDeserializeElement(parseSerializedButtonBlockElement),
}),
},
normalizeNode: [fixDuplicateButtonBlockUuid, normalizeRedundantButtonBlockAttributes],
renderElement: ({ attributes, children, element }: RenderElementProps) => {
if (ButtonBlockNode.isButtonBlockNode(element)) {
return (
<ButtonBlockElement
attributes={attributes}
element={element}
withNewTabOption={withNewTabOption}
>
{children}
</ButtonBlockElement>
);
}

return undefined;
},
isVoid: ButtonBlockNode.isButtonBlockNode,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ElementNode } from '@prezly/slate-types';
import { isElementNode } from '@prezly/slate-types';
import type { Node } from 'slate';

type Uuid = string;

export interface ButtonBlockNode extends ElementNode {
type: typeof ButtonBlockNode.Type;
uuid: Uuid;
href: string;
layout: ButtonBlockNode.Layout;
variant: ButtonBlockNode.Variant;
e1himself marked this conversation as resolved.
Show resolved Hide resolved
new_tab: boolean;
label: string;
}

export namespace ButtonBlockNode {
export const Type = 'button-block';

export enum Layout {
LEFT = 'left',
RIGHT = 'right',
CENTER = 'center',
WIDE = 'wide',
}

export enum Variant {
DEFAULT = 'default',
OUTLINE = 'outline',
}

export function isButtonBlockNode(node: Node): node is ButtonBlockNode {
return isElementNode(node, Type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@import "styles/variables";
@import "styles/helpers";

.Button {
cursor: pointer;
display: flex;
padding: $spacing-1-5 $spacing-2;
justify-content: center;
align-items: center;
border-radius: $border-radius-small;
color: $white;
background-color: rgba($color: $editor-link-color, $alpha: 0.5);
font-size: $font-size-medium;
font-weight: 600;
line-height: 160%;

&.notWide {
width: max-content;
}

&.active {
background-color: $editor-link-color;
}

&.outline {
border: 2px solid rgba($color: $editor-link-color, $alpha: 0.5);
color: rgba($color: $editor-link-color, $alpha: 0.5);
background-color: $white;

&.active {
color: $editor-link-color;
border-color: $editor-link-color;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import classNames from 'classnames';
import React from 'react';

import type { ButtonBlockNode } from '../../ButtonBlockNode';

import styles from './Button.module.scss';

interface Props {
node: ButtonBlockNode;
}

export function Button({ node }: Props) {
const { label, variant, layout, href } = node;

return (
<div
className={classNames(styles.Button, {
[styles.active]: Boolean(href),
[styles.outline]: variant === 'outline',
[styles.notWide]: layout !== 'wide',
})}
role="button"
>
{label}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Button';
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useCallback } from 'react';
import { useSlateStatic, type RenderElementProps } from 'slate-react';

import { EditorBlock } from '#components';

import { EventsEditor } from '#modules/events';

import type { ButtonBlockNode } from '../ButtonBlockNode';
import { removeButtonBlock, updateButtonBlock } from '../transforms';

import { Button } from './Button/Button';
import { ButtonMenu, type FormState } from './ButtonBlockMenu';

interface Props extends RenderElementProps {
element: ButtonBlockNode;
withNewTabOption: boolean;
}

export function ButtonBlockElement({ attributes, children, element, withNewTabOption }: Props) {
const editor = useSlateStatic();

const { layout } = element;

const align = layout === 'wide' ? 'center' : layout;

const handleUpdate = useCallback(
function (patch: Partial<FormState>) {
updateButtonBlock(editor, element, patch);
},
[editor, element],
);

const handleRemove = useCallback(
function () {
if (removeButtonBlock(editor, element)) {
EventsEditor.dispatchEvent(editor, 'button-block-removed', {
uuid: element.uuid,
});
}
},
[editor, element],
);

return (
<EditorBlock
{...attributes}
element={element}
align={align}
overlay="autohide"
// We have to render children or Slate will fail when trying to find the node.
renderAboveFrame={children}
renderReadOnlyFrame={() => <Button node={element} />}
renderMenu={({ onClose }) => (
<ButtonMenu
onClose={onClose}
onUpdate={handleUpdate}
onRemove={handleRemove}
value={{
href: element.href,
label: element.label,
layout: element.layout,
new_tab: element.new_tab,
variant: element.variant,
}}
withNewTabOption={withNewTabOption}
/>
)}
width={layout !== 'wide' ? 'min-content' : undefined}
rounded
void
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import "styles/variables";

.icon {
fill: $white;

&.active {
fill: $yellow-300;
}
}
Loading
Loading