Skip to content

Commit

Permalink
feat(components): 'Editable' passes through html props. [Canary] (#2020)
Browse files Browse the repository at this point in the history
## Changes
- Allow the bodiless "Editable" component to receive html props and pass them on to the span it renders.
- Moves node key for vital copyright row up to footer.

## Test Instructions
- On the vital-demo site, verify that the hero card title has the following props:
  - `data-layer-region="Card:Title"`
  - `data-shadowed-by="__vitaltest__:Card:HeroLeftImageCentered"`
 - Validate that copyright renders correctly and is still editable
  • Loading branch information
wodenx committed Apr 6, 2023
1 parent 07debbb commit 3897084
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 41 deletions.
67 changes: 64 additions & 3 deletions packages/bodiless-components/__tests__/Editable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,70 @@
import React from 'react';
import flow from 'lodash/flow';

import { render } from 'enzyme';
import { render, mount } from 'enzyme';
import { PageEditContext } from '@bodiless/core';
import { withMockNode } from './helpers/MockContentNode';
import { asEditable } from '../src';
import { Editable, asEditable } from '../src';

describe('Editable', () => {
describe('When editable', () => {
let mockIsEdit: jest.SpyInstance;

beforeAll(() => {
mockIsEdit = jest.spyOn(PageEditContext.prototype, 'isEdit', 'get').mockReturnValue(true);
});

afterAll(() => {
mockIsEdit.mockRestore();
});

it('Passes props to span', () => {
const wrapper = mount(<Editable data-foo="bar" nodeKey="bar">Now is the time</Editable>);
expect(wrapper.find('span').prop('data-foo')).toBe('bar');
});

it('Passes classname to span', () => {
const wrapper = mount(<Editable nodeKey="bar" className="baz">Now is the time</Editable>);
expect(wrapper.find('span').prop('className')?.split(' ')).toContain('baz');
});

it('Adds inline-editable class when className provided', () => {
const wrapper = mount(<Editable nodeKey="bar" className="baz">Now is the time</Editable>);
expect(wrapper.find('span').prop('className')?.split(' ')).toContain('bodiless-inline-editable');
});

it('Adds inline-editable class when no className provided', () => {
const wrapper = mount(<Editable nodeKey="bar">Now is the time</Editable>);
expect(wrapper.find('span').prop('className')).toBe('bodiless-inline-editable');
});

it('Adds test id for playwright', () => {
const wrapper = mount(<Editable nodeKey="bar">Now is the time</Editable>);
expect(wrapper.find('span').prop('data-test-id')).toBe('bodiless-inline-editable');
});

it('Accepts a custom tag', () => {
const wrapper = mount(<Editable data-foo="bar" nodeKey="bar" tagName="h1">Now is the time</Editable>);
expect(wrapper.find('h1').prop('data-foo')).toBe('bar');
});
});

describe('When not editable', () => {
it('Passes props to span', () => {
const wrapper = mount(<Editable data-foo="bar" nodeKey="bar">Now is the time</Editable>);
expect(wrapper.find('span').prop('data-foo')).toBe('bar');
});
it('Passes classname to span', () => {
const wrapper = mount(<Editable nodeKey="bar" className="baz">Now is the time</Editable>);
expect(wrapper.find('span').prop('className')?.split(' ')).toContain('baz');
});

it('Accepts a custom tag', () => {
const wrapper = mount(<Editable data-foo="bar" nodeKey="bar" tagName="h1">Now is the time</Editable>);
expect(wrapper.find('h1').prop('data-foo')).toBe('bar');
});
});
});

describe('asEditable', () => {
describe('When not editable', () => {
Expand All @@ -30,7 +91,7 @@ describe('asEditable', () => {
const Test = flow(
asEditable('foo', 'Foo', useOverrides),
withMockNode(data),
)('apan');
)('span');
const wrapper = render(<Test />);
expect(wrapper.text()).toBe('Now *s the t*me');
});
Expand Down
36 changes: 24 additions & 12 deletions packages/bodiless-components/src/Editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ type EditableOverrides = {

export type UseEditableOverrides = (props: EditableProps) => EditableOverrides;

type EditableProps = {
type EditableBaseProps = {
placeholder?: string,
children?: string,
useOverrides?: UseEditableOverrides,
} & Partial<WithNodeProps>;
tagName?: keyof JSX.IntrinsicElements,
className?: string,
};

type EditableProps = EditableBaseProps & Partial<WithNodeProps>;

export type EditableData = {
text: string;
Expand All @@ -59,21 +63,26 @@ export const isEditableData = (d: any): d is EditableData => {
return true;
};

const Text = observer((props: EditableProps) => {
const { placeholder, useOverrides = () => ({}) }: EditableProps = props;
const Text = observer((props: EditableBaseProps) => {
const {
placeholder, useOverrides = () => ({}), children, tagName: Tag = 'span', ...rest
}: EditableProps = props;
const { sanitizer = identity }: EditableOverrides = useOverrides(props);
const { node } = useNode<EditableData>();
const text = sanitizer(
(node.data.text !== undefined ? node.data.text : props.children) || placeholder || '',
(node.data.text !== undefined ? node.data.text : children) || placeholder || '',
);
// eslint-disable-next-line react/no-danger
return <span dangerouslySetInnerHTML={{ __html: text }} />;
return <Tag {...rest} dangerouslySetInnerHTML={{ __html: text }} />;
});
const EditableText = observer((props: EditableProps) => {
const EditableText = observer((props: EditableBaseProps) => {
const { node } = useNode<EditableData>();
const { placeholder = '', useOverrides = () => ({}) }: EditableProps = props;
const {
placeholder = '', useOverrides = () => ({}), children, tagName = 'span',
className, ...rest
} = props;
const { sanitizer = identity }: EditableOverrides = useOverrides(props);
const text = (node.data.text !== undefined ? node.data.text : props.children) || '';
const text = (node.data.text !== undefined ? node.data.text : children) || '';
const [hasFocus, setFocus] = useState(false);
const ref = useRef<HTMLElement>(null);
const onChange = useCallback(() => {
Expand All @@ -89,23 +98,26 @@ const EditableText = observer((props: EditableProps) => {
document.execCommand('insertHTML', false, pasteText);
}
}, []);
const finalClassName = (className?.split(' ') || []).concat('bodiless-inline-editable').join(' ');
return (
<ContentEditable
{...rest}
innerRef={ref}
tagName="span"
className="bodiless-inline-editable"
tagName={tagName}
className={finalClassName}
onChange={onChange}
onPaste={pasteAsPlainText}
onFocus={onFocus}
onBlur={onBlur}
html={hasFocus ? text : sanitizer(text)}
data-placeholder={placeholder}
data-test-id="bodiless-inline-editable"
/>
);
});

const Editable = withNode(
observer((props: EditableProps) => {
observer((props: EditableBaseProps) => {
const { isEdit } = useEditContext();
return isEdit ? (
<EditableText {...props} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
*/

import { Editable } from '@bodiless/components';
import { withoutHydrationInline } from '@bodiless/hydration';
import { withoutHydration, withoutHydrationInline } from '@bodiless/hydration';
import { stylable } from '@bodiless/fclasses';

export const EditorPlainClean = withoutHydrationInline()(Editable);
export const EditorPlainClean = withoutHydrationInline()(stylable(Editable));
export const BlockEditorPlainClean = withoutHydration()(stylable(Editable));
export { default as vitalEditorPlain } from './tokens';
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

import {
StaticInline as EditorPlainClean,
StaticBlock as BlockEditorPlainClean,
staticTokenCollection as vitalEditorPlain,
} from '@bodiless/hydration';

export { EditorPlainClean, vitalEditorPlain };
export { EditorPlainClean, BlockEditorPlainClean, vitalEditorPlain };
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { CopyrightRowComponents, CopyrightRowProps } from './types';
const copyrightRowComponents: CopyrightRowComponents = {
Wrapper: Div,
CopyrightWrapper: Div,
Disclaimer: RichTextClean,
Copyright: RichTextClean,
SocialLinksWrapper: Div,
SocialLinks: SocialLinksClean,
Expand All @@ -31,6 +32,7 @@ const copyrightRowComponents: CopyrightRowComponents = {
const CopyrightRowCleanBase: FC<CopyrightRowProps> = ({ components: C, ...rest }) => (
<C.Wrapper {...rest}>
<C.CopyrightWrapper>
<C.Disclaimer />
<C.Copyright />
</C.CopyrightWrapper>
<C.SocialLinksWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@
* limitations under the License.
*/

import { withNodeKey } from '@bodiless/core';
import { vitalRichText } from '@bodiless/vital-editors';
import {
asVitalTokenSpec, vitalColor, vitalFontSize, vitalTextDecoration
asFluidToken, vitalColor, vitalFontSize, vitalTextDecoration
} from '@bodiless/vital-elements';
import { addProps, as, replaceWith } from '@bodiless/fclasses';
import { vitalLink } from '@bodiless/vital-link';
import { withNodeKey } from '@bodiless/core';
import { asCopyrightRowToken } from '../CopyrightRowClean';
import type { CopyrightRowToken } from '../CopyrightRowClean';
import { vitalSocialLinks } from '../../SocialLinks';

const Copyright = asVitalTokenSpec()({
const vitalCopyrightRowRichTextDefault = asFluidToken({
...vitalRichText.Basic,
Theme: {
paragraph: as(
Expand All @@ -42,16 +42,19 @@ const Copyright = asVitalTokenSpec()({
},
});

const Base = asCopyrightRowToken({
const Default = asCopyrightRowToken({
Components: {
SocialLinks: vitalSocialLinks.Default,
Copyright: vitalCopyrightRowRichTextDefault,
Disclaimer: vitalCopyrightRowRichTextDefault,
},
Layout: {
CopyrightWrapper: 'w-full xl:w-3/4',
SocialLinksWrapper: 'w-full xl:w-1/4',
},
Spacing: {
Copyright: 'py-6 2xl:py-0 md:mb-4 2xl:mb-0', // Vertical
Disclaimer: 'mb-2',
SocialLinksWrapper: 'py-6 2xl:py-0', // Vertical
},
Theme: {
Expand All @@ -60,18 +63,16 @@ const Base = asCopyrightRowToken({
'border-t border-b md:border-0',
),
},
Editors: {
Copyright,
},
Schema: {
Copyright: withNodeKey({ nodeKey: 'copyright', nodeCollection: 'site' }),
Copyright: withNodeKey('copyright'),
Disclaimer: withNodeKey('disclaimer'),
},
});

const CopyrightNoSocialLinks = asCopyrightRowToken({
...Base,
const NoSocialLinks = asCopyrightRowToken({
...Default,
Components: {
...Base.Components,
...Default.Components,
SocialLinksWrapper: replaceWith(() => null),
SocialLinks: replaceWith(() => null),
},
Expand All @@ -81,8 +82,20 @@ const CopyrightNoSocialLinks = asCopyrightRowToken({
},
});

const Default = asCopyrightRowToken({
...Base,
const NoDisclaimer = asCopyrightRowToken({
...Default,
Components: {
...Default.Components,
Disclaimer: replaceWith(() => null),
},
});

const CopyrightOnly = asCopyrightRowToken({
...NoSocialLinks,
Components: {
...NoSocialLinks.Components,
Disclaimer: replaceWith(() => null),
},
});

/**
Expand All @@ -93,19 +106,23 @@ const Default = asCopyrightRowToken({
*/
export interface VitalCopyrightRow {
/**
* Base applies the following:
* Default applies the following:
* - Vital Styled Copyright editor on left
* - Social Links on right
*/
Base: CopyrightRowToken,
Default: CopyrightRowToken,
/**
* Inherits Base
* Same as Default but no social links
*/
Default: CopyrightRowToken,
NoSocialLinks: CopyrightRowToken,
/**
* Same as Default but no disclaimer
*/
NoDisclaimer: CopyrightRowToken,
/**
* Copyright only
* Same as Default but only Copyright (no social links or disclaimer).
*/
CopyrightNoSocialLinks: CopyrightRowToken,
CopyrightOnly: CopyrightRowToken,
}

/**
Expand All @@ -116,9 +133,10 @@ export interface VitalCopyrightRow {
* @see [[CopyrightRowClean]]
*/
const vitalCopyrightRow: VitalCopyrightRow = {
Base,
Default,
CopyrightNoSocialLinks,
NoSocialLinks,
NoDisclaimer,
CopyrightOnly,
};

export default vitalCopyrightRow;
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const Base = asFooterToken({
Wrapper: vitalColor.BgSecondaryFooter,
},
Schema: {
FooterMenu: withNodeKey({ nodeKey: 'footer-menu', nodeCollection: 'site' }),
FooterMenu: withNodeKey('footer-menu'),
_: withNode,
},
});
Expand Down
7 changes: 7 additions & 0 deletions packages/vital-test/src/shadow/@bodiless/vital-card/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ const Hero = asCardToken(vitalCardBase.Hero, {
},
});

const HeroLeftImageContentCentered = asCardToken(vitalCardBase.HeroLeftImageContentCentered, {
Content: {
Title: addProps({ 'data-shadowed-by': '__vitaltest__:Card:HeroLeftImageCentered' }),
},
});

export default {
...vitalCardBase,
Basic,
Hero,
HeroLeftImageContentCentered,
};
2 changes: 1 addition & 1 deletion playwright/pages/link-toggle-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class LinkTogglePage extends BasePage {
this.url = 'AT-Url';
this.normalizedUrl = `/${this.url}/`;
this.editedPostfix = 'edited';
this.labelXpath = '//*[@data-linktoggle-element="link-toggle"]//*[@class="bodiless-inline-editable"]';
this.labelXpath = '//*[@data-linktoggle-element="link-toggle"]//*[@data-test-id="bodiless-inline-editable"]';
this.labelPreviewXpath = '//*[@data-linktoggle-element="link-toggle"]//span';
this.linkXpath = '//*[@data-linktoggle-element="link-toggle"]//a';
this.linkIconAddXpath = '//*[@aria-label="Local Context Menu"]//*[@aria-label="Add Link"]';
Expand Down
9 changes: 9 additions & 0 deletions sites/test-site/src/data/pages/sitedatapage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ export default (props: any) => (
<div className="m-2 p-2 w-1/3 h-12 border-blue border">
<Editable nodeKey="sitedatapage" nodeCollection="site" placeholder="Site level data..." />
</div>
<h4 className="text-base font-bold">The following is site level with custom tag and prop</h4>
<div className="m-2 p-2 w-1/3 h-12 border-blue border">
<Editable
nodeKey="sitedatapage-2"
placeholder="Page level data with custom tag..."
tagName="section"
data-custom-attr="custom"
/>
</div>
</Layout>
</Page>
);
Expand Down
10 changes: 10 additions & 0 deletions sites/vital-demo/src/data/site/footer$disclaimer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"type": "paragraph",
"children": [
{
"text": "This site is only a demonstration. "
}
]
}
]

1 comment on commit 3897084

@vercel
Copy link

@vercel vercel bot commented on 3897084 Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.