Skip to content

Commit

Permalink
feat: hyperlink edit popover modal
Browse files Browse the repository at this point in the history
  • Loading branch information
YvesRijckaert committed Nov 15, 2023
1 parent 4eb475c commit 3427085
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 107 deletions.
30 changes: 5 additions & 25 deletions cypress/e2e/rich-text/RichTextEditor.Links.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
inline,
text,
} from '../../../packages/rich-text/src/helpers/nodeFactory';
import { getIframe } from '../../fixtures/utils';
import { getIframe, openEditLink } from '../../fixtures/utils';
import { RichTextPage } from './RichTextPage';

// the sticky toolbar gets in the way of some of the tests, therefore
Expand Down Expand Up @@ -270,12 +270,7 @@ describe('Rich Text Editor - Links', { viewportHeight: 2000 }, () => {

// Part 2:
// Update hyperlink to entry link

richText.editor
.findByTestId('cf-ui-text-link')
.should('have.text', 'My cool website')
.click({ force: true });

openEditLink();
form.linkText.should('not.exist');
form.linkType.should('have.value', 'hyperlink').select('entry-hyperlink');
form.linkEntityTarget.should('have.text', 'Select entry').click();
Expand All @@ -293,12 +288,7 @@ describe('Rich Text Editor - Links', { viewportHeight: 2000 }, () => {

// Part 3:
// Update entry link to asset link

richText.editor
.findByTestId('cf-ui-text-link')
.should('have.text', 'My cool website')
.click({ force: true });

openEditLink();
form.linkText.should('not.exist');
form.linkType.should('have.value', 'entry-hyperlink').select('asset-hyperlink');
form.linkEntityTarget.should('have.text', 'Select asset').click();
Expand All @@ -316,12 +306,7 @@ describe('Rich Text Editor - Links', { viewportHeight: 2000 }, () => {

// Part 4:
// Update asset link to resource link

richText.editor
.findByTestId('cf-ui-text-link')
.should('have.text', 'My cool website')
.click({ force: true });

openEditLink();
form.linkText.should('not.exist');
form.linkType.should('have.value', 'asset-hyperlink').select('resource-hyperlink');
form.linkEntityTarget.should('have.text', 'Select entry').click();
Expand All @@ -347,12 +332,7 @@ describe('Rich Text Editor - Links', { viewportHeight: 2000 }, () => {

// Part 5:
// Update resource link to hyperlink

richText.editor
.findByTestId('cf-ui-text-link')
.should('have.text', 'My cool website')
.click({ force: true });

openEditLink();
form.linkText.should('not.exist');
form.linkType.should('have.value', 'resource-hyperlink').select('hyperlink');
form.linkTarget.type('https://zombo.com');
Expand Down
11 changes: 7 additions & 4 deletions cypress/e2e/rich-text/RichTextEditor.Tracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { BLOCKS, INLINES, MARKS } from '@contentful/rich-text-types';

import { getIframe } from '../../fixtures/utils';
import { getIframe, openEditLink } from '../../fixtures/utils';
import { RichTextPage } from './RichTextPage';

// the sticky toolbar gets in the way of some of the tests, therefore
Expand Down Expand Up @@ -593,8 +593,9 @@ describe('Rich Text Editor - Tracking', { viewportHeight: 2000 }, () => {
// Part 2:
// Update hyperlink to entry link

richText.editor.findByTestId('cf-ui-text-link').click({ force: true });
richText.editor.findByTestId('cf-ui-text-link');

openEditLink();
form.linkType.select('entry-hyperlink');
form.linkEntityTarget.click();
form.submit.click();
Expand All @@ -610,8 +611,9 @@ describe('Rich Text Editor - Tracking', { viewportHeight: 2000 }, () => {
// Part 3:
// Update entry link to asset link

richText.editor.findByTestId('cf-ui-text-link').click({ force: true });
richText.editor.findByTestId('cf-ui-text-link');

openEditLink();
form.linkType.select('asset-hyperlink');
form.linkEntityTarget.click();
form.submit.click();
Expand All @@ -630,8 +632,9 @@ describe('Rich Text Editor - Tracking', { viewportHeight: 2000 }, () => {
// Part 4:
// Update asset link to hyperlink

richText.editor.findByTestId('cf-ui-text-link').click({ force: true });
richText.editor.findByTestId('cf-ui-text-link');

openEditLink();
form.linkType.select('hyperlink');
form.linkTarget.type('https://zombo.com');
form.submit.click();
Expand Down
9 changes: 9 additions & 0 deletions cypress/fixtures/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ export const getIframe = (): Cypress.Chainable<JQuery<HTMLBodyElement>> => {
.then((body) => cy.wrap(body as HTMLBodyElement));
};

export const openEditLink = () => {
return getIframe()
.findByTestId('cf-ui-popover-content')
.should('exist')
.findByLabelText('Edit link')
.should('exist')
.click();
};

export const getIframeWindow = () => {
return cy
.get('#storybook-preview-iframe')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as React from 'react';

import { FieldAppSDK, Link } from '@contentful/app-sdk';
import { Tooltip, TextLink } from '@contentful/f36-components';
import { Text } from '@contentful/f36-components';

import { useContentfulEditor } from '../../../ContentfulEditorProvider';
import { fromDOMPoint } from '../../../internal';
import { findNodePath, isChildPath } from '../../../internal/queries';
import { Element, RenderElementProps } from '../../../internal/types';
import { useSdkContext } from '../../../SdkProvider';
import { useLinkTracking } from '../../links-tracking';
import { addOrEditLink } from '../HyperlinkModal';
import { useEntityInfo } from '../useEntityInfo';
import { handleEditLink, handleRemoveLink } from './linkHandlers';
import { LinkPopover } from './LinkPopover';
import { styles } from './styles';

export type HyperlinkElementProps = {
Expand All @@ -33,45 +34,45 @@ export type HyperlinkElementProps = {
export function EntityHyperlink(props: HyperlinkElementProps) {
const editor = useContentfulEditor();
const sdk: FieldAppSDK = useSdkContext();
const focus = editor.selection?.focus;
const { target } = props.element.data;
const { onEntityFetchComplete } = useLinkTracking();
const pathToElement = findNodePath(editor, props.element);
const isLinkFocused = pathToElement && focus && isChildPath(focus.path, pathToElement);

const tooltipContent = useEntityInfo({
target,
sdk,
onEntityFetchComplete,
});

if (!target) return null;

function handleClick(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
event.stopPropagation();
if (!editor) return;

const p = fromDOMPoint(editor, [event.target as Node, 0]);

if (p) {
addOrEditLink(editor, sdk, editor.tracking.onViewportAction, p.path);
}
if (!target) {
return null;
}

const popoverText = (
<Text fontColor="blue600" fontWeight="fontWeightMedium" className={styles.openLink}>
{tooltipContent}
</Text>
);

return (
<Tooltip
content={tooltipContent}
targetWrapperClassName={styles.hyperlinkWrapper}
placement="bottom"
maxWidth="auto"
<LinkPopover
isLinkFocused={isLinkFocused}
handleEditLink={() => handleEditLink(editor, sdk, pathToElement)}
handleRemoveLink={() => handleRemoveLink(editor)}
popoverText={popoverText}
>
<TextLink
as="a"
onClick={handleClick}
<Text
testId="cf-ui-text-link"
fontColor="blue600"
fontWeight="fontWeightMedium"
className={styles.hyperlink}
data-link-type={target.sys.linkType}
data-link-id={target.sys.id}
>
{props.children}
</TextLink>
</Tooltip>
</Text>
</LinkPopover>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as React from 'react';

import { Popover, IconButton, Tooltip, Flex } from '@contentful/f36-components';
import { EditIcon, CopyIcon } from '@contentful/f36-icons';

import { styles } from './styles';

type LinkPopoverProps = {
isLinkFocused: boolean | undefined;
popoverText: React.ReactNode;
handleEditLink: () => void;
handleRemoveLink: () => void;
children: React.ReactNode;
handleCopyLink?: () => void;
};

export const LinkPopover = ({
isLinkFocused,
popoverText,
handleEditLink,
handleRemoveLink,
children,
handleCopyLink,
}: LinkPopoverProps) => (
// eslint-disable-next-line jsx-a11y/no-autofocus -- we don't want to autofocus the popover
<Popover usePortal={false} autoFocus={false} isOpen={isLinkFocused}>
<Popover.Trigger>{children}</Popover.Trigger>
<Popover.Content className={styles.popover}>
<Flex
alignItems="center"
paddingTop="spacing2Xs"
paddingBottom="spacing2Xs"
paddingRight="spacing2Xs"
paddingLeft="spacingXs"
>
{popoverText}
{handleCopyLink && (
<Tooltip placement="bottom" content="Copy link" usePortal>
<IconButton
className={styles.iconButton}
onClick={handleCopyLink}
size="small"
variant="transparent"
aria-label="Copy link"
icon={<CopyIcon size="tiny" />}
/>
</Tooltip>
)}
<Tooltip placement="bottom" content="Edit link" usePortal>
<IconButton
className={styles.iconButton}
onClick={handleEditLink}
size="small"
variant="transparent"
aria-label="Edit link"
icon={<EditIcon size="tiny" />}
/>
</Tooltip>
<Tooltip placement="bottom" content="Remove link" usePortal>
<IconButton
onClick={handleRemoveLink}
className={styles.iconButton}
size="small"
variant="transparent"
aria-label="Remove link"
icon={
//@TODO: Replace icon when available in f36
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.75 8C1.75 8.59674 1.98705 9.16903 2.40901 9.59099C2.83097 10.0129 3.40326 10.25 4 10.25H6.5C6.69891 10.25 6.88968 10.329 7.03033 10.4697C7.17098 10.6103 7.25 10.8011 7.25 11C7.25 11.1989 7.17098 11.3897 7.03033 11.5303C6.88968 11.671 6.69891 11.75 6.5 11.75H4C3.00544 11.75 2.05161 11.3549 1.34835 10.6517C0.645088 9.94839 0.25 8.99456 0.25 8C0.25 7.00544 0.645088 6.05161 1.34835 5.34835C2.05161 4.64509 3.00544 4.25 4 4.25H6.5C6.69891 4.25 6.88968 4.32902 7.03033 4.46967C7.17098 4.61032 7.25 4.80109 7.25 5C7.25 5.19891 7.17098 5.38968 7.03033 5.53033C6.88968 5.67098 6.69891 5.75 6.5 5.75H4C3.40326 5.75 2.83097 5.98705 2.40901 6.40901C1.98705 6.83097 1.75 7.40326 1.75 8ZM12 4.25H9.5C9.30109 4.25 9.11032 4.32902 8.96967 4.46967C8.82902 4.61032 8.75 4.80109 8.75 5C8.75 5.19891 8.82902 5.38968 8.96967 5.53033C9.11032 5.67098 9.30109 5.75 9.5 5.75H12C12.5967 5.75 13.169 5.98705 13.591 6.40901C14.0129 6.83097 14.25 7.40326 14.25 8C14.25 8.59674 14.0129 9.16903 13.591 9.59099C13.169 10.0129 12.5967 10.25 12 10.25H9.5C9.30109 10.25 9.11032 10.329 8.96967 10.4697C8.82902 10.6103 8.75 10.8011 8.75 11C8.75 11.1989 8.82902 11.3897 8.96967 11.5303C9.11032 11.671 9.30109 11.75 9.5 11.75H12C12.9946 11.75 13.9484 11.3549 14.6517 10.6517C15.3549 9.94839 15.75 8.99456 15.75 8C15.75 7.00544 15.3549 6.05161 14.6517 5.34835C13.9484 4.64509 12.9946 4.25 12 4.25Z"
fill="black"
/>
</svg>
}
/>
</Tooltip>
</Flex>
</Popover.Content>
</Popover>
);
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as React from 'react';

import { FieldAppSDK, Link } from '@contentful/app-sdk';
import { Tooltip, TextLink } from '@contentful/f36-components';
import { Text } from '@contentful/f36-components';

import { useContentfulEditor } from '../../../ContentfulEditorProvider';
import { fromDOMPoint } from '../../../internal';
import { findNodePath, isChildPath } from '../../../internal/queries';
import { Element, RenderElementProps } from '../../../internal/types';
import { useLinkTracking } from '../../../plugins/links-tracking';
import { useSdkContext } from '../../../SdkProvider';
import { addOrEditLink } from '../HyperlinkModal';
import { useLinkTracking } from '../../links-tracking';
import { useResourceEntityInfo } from '../useResourceEntityInfo';
import { handleEditLink, handleRemoveLink } from './linkHandlers';
import { LinkPopover } from './LinkPopover';
import { styles } from './styles';

export type ResourceHyperlinkProps = {
Expand All @@ -32,41 +33,41 @@ export type ResourceHyperlinkProps = {
export function ResourceHyperlink(props: ResourceHyperlinkProps) {
const editor = useContentfulEditor();
const sdk: FieldAppSDK = useSdkContext();
const focus = editor.selection?.focus;
const { target } = props.element.data;
const { onEntityFetchComplete } = useLinkTracking();
const pathToElement = findNodePath(editor, props.element);
const isLinkFocused = pathToElement && focus && isChildPath(focus.path, pathToElement);

const tooltipContent = useResourceEntityInfo({ target, onEntityFetchComplete });

if (!target) return null;

function handleClick(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
event.stopPropagation();
if (!editor) return;

const p = fromDOMPoint(editor, [event.target as Node, 0]);

if (p) {
addOrEditLink(editor, sdk, editor.tracking.onViewportAction, p.path);
}
if (!target) {
return null;
}

const popoverText = (
<Text fontColor="blue600" fontWeight="fontWeightMedium" className={styles.openLink}>
{tooltipContent}
</Text>
);

return (
<Tooltip
content={tooltipContent}
targetWrapperClassName={styles.hyperlinkWrapper}
placement="bottom"
maxWidth="auto"
<LinkPopover
isLinkFocused={isLinkFocused}
handleEditLink={() => handleEditLink(editor, sdk, pathToElement)}
handleRemoveLink={() => handleRemoveLink(editor)}
popoverText={popoverText}
>
<TextLink
as="a"
onClick={handleClick}
<Text
testId="cf-ui-text-link"
fontColor="blue600"
fontWeight="fontWeightMedium"
className={styles.hyperlink}
data-resource-link-type={target.sys.linkType}
data-resource-link-urn={target.sys.urn}
>
{props.children}
</TextLink>
</Tooltip>
</Text>
</LinkPopover>
);
}
Loading

0 comments on commit 3427085

Please sign in to comment.