From 5d713fda086d0e40d619e1a955f9849ddff7397a Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Mon, 14 Jun 2021 10:27:46 +0100 Subject: [PATCH 1/5] fix(toolbar): add missing focus trigger when right key press and last button focused --- .../text-editor/__internal__/toolbar/toolbar.component.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/text-editor/__internal__/toolbar/toolbar.component.js b/src/components/text-editor/__internal__/toolbar/toolbar.component.js index 8794c3a102..b96338a304 100644 --- a/src/components/text-editor/__internal__/toolbar/toolbar.component.js +++ b/src/components/text-editor/__internal__/toolbar/toolbar.component.js @@ -64,6 +64,7 @@ const Toolbar = ({ setTabbable(false); } else if (Events.isRightKey(ev)) { if (focusIndex === 3) { + controlRefs[0].current.focus(); setFocusIndex(0); } else { controlRefs[focusIndex + 1].current.focus(); From 4348aaf65e095dce3d0393449d579286d9bab931 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Wed, 16 Jun 2021 16:22:27 +0100 Subject: [PATCH 2/5] feat(link-preview): add new component to support displaying link previews Creates a new `LinkPreview` component to render data relating to links added in components like the `TextEditor` as a `div` element. They will also display in the `Note` component as a read only `a` element. The component has a loading state controlled by the `isLoading` prop and supports passing in a `title`, a `description` and an `image` config object along with the url for the associated `EditorLink`. If no `image` config is passed a placeholder image is used. --- jest.conf.json | 4 + .../__internal__/placeholder.component.js | 55 +++++ src/components/link-preview/index.d.ts | 2 + src/components/link-preview/index.js | 1 + .../link-preview/link-preview.component.js | 107 +++++++++ src/components/link-preview/link-preview.d.ts | 24 ++ .../link-preview/link-preview.spec.js | 215 ++++++++++++++++++ .../link-preview/link-preview.stories.js | 31 +++ .../link-preview/link-preview.stories.mdx | 70 ++++++ .../link-preview/link-preview.style.js | 106 +++++++++ .../editor-link/editor-link.component.js | 1 + src/style/themes/base/base-theme.config.js | 7 + 12 files changed, 623 insertions(+) create mode 100644 src/components/link-preview/__internal__/placeholder.component.js create mode 100644 src/components/link-preview/index.d.ts create mode 100644 src/components/link-preview/index.js create mode 100644 src/components/link-preview/link-preview.component.js create mode 100644 src/components/link-preview/link-preview.d.ts create mode 100644 src/components/link-preview/link-preview.spec.js create mode 100644 src/components/link-preview/link-preview.stories.js create mode 100644 src/components/link-preview/link-preview.stories.mdx create mode 100644 src/components/link-preview/link-preview.style.js diff --git a/jest.conf.json b/jest.conf.json index 8938b5fe0a..6f0dcdf3c9 100644 --- a/jest.conf.json +++ b/jest.conf.json @@ -21,5 +21,9 @@ "transform": { "^.+\\.(js|jsx|ts|tsx)$": "babel-jest", "^.+\\.svg$": "/svgTransform.js" + }, + "moduleNameMapper": { + "\\.(png)$": "/__mocks__/imageMock.js" } + } diff --git a/src/components/link-preview/__internal__/placeholder.component.js b/src/components/link-preview/__internal__/placeholder.component.js new file mode 100644 index 0000000000..fe75fbbe10 --- /dev/null +++ b/src/components/link-preview/__internal__/placeholder.component.js @@ -0,0 +1,55 @@ +import React from "react"; +import styled from "styled-components"; +import { toColor } from "../../../style/utils/color"; +import { baseTheme } from "../../../style/themes"; + +const StyledPlaceHolder = styled.div` + overflow: hidden; + position: relative; + height: 152px; + min-width: 152px; + background-color: ${({ theme }) => theme.editorLinkPreview.background}; +`; + +const Circle = styled.div` + height: 22px; + width: 22px; + border-radius: 50%; + background-color: ${({ theme }) => theme.editorLinkPreview.hoverBackground}; + position: absolute; + left: 22px; + top: 30px; +`; + +const Square = styled.div` + height: 200px; + width: 200px; + transform: rotate(45deg); + background-color: ${({ color, theme }) => toColor(theme, color)}; + position: absolute; + border-radius: 2%; + top: ${({ top }) => top}; + left: ${({ left }) => left}; +`; + +StyledPlaceHolder.defaultProps = { + theme: baseTheme, +}; + +Circle.defaultProps = { + theme: baseTheme, +}; + +Square.defaultProps = { + theme: baseTheme, +}; + +const Placeholder = () => ( + + + + + +); + +export default Placeholder; diff --git a/src/components/link-preview/index.d.ts b/src/components/link-preview/index.d.ts new file mode 100644 index 0000000000..32ab8305a7 --- /dev/null +++ b/src/components/link-preview/index.d.ts @@ -0,0 +1,2 @@ +export { default } from "./link-preview"; +export * from "./link-preview"; diff --git a/src/components/link-preview/index.js b/src/components/link-preview/index.js new file mode 100644 index 0000000000..cb0dcc0355 --- /dev/null +++ b/src/components/link-preview/index.js @@ -0,0 +1 @@ +export { default } from "./link-preview.component"; diff --git a/src/components/link-preview/link-preview.component.js b/src/components/link-preview/link-preview.component.js new file mode 100644 index 0000000000..31c5daeccd --- /dev/null +++ b/src/components/link-preview/link-preview.component.js @@ -0,0 +1,107 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { + StyledLinkPreview, + StyledPreviewWrapper, + StyledCloseIconWrapper, + StyledTitle, + StyledDescription, + StyledUrl, +} from "./link-preview.style"; +import Image from "../image"; +import Preview from "../preview"; +import IconButton from "../icon-button"; +import Icon from "../icon"; +import Placeholder from "./__internal__/placeholder.component"; + +const SCHEME_SEPARATOR = "://"; + +const LinkPreview = ({ + as, + description, + image, + isLoading, + onClose, + title, + url, + ...rest +}) => { + const loadingState = isLoading || !url; + const canRenderAsLink = !loadingState && as !== "div"; + + const imageProps = () => { + return { + src: image?.url, + alt: image?.alt || "Link preview image", + height: "152px", + }; + }; + + const displayUrl = () => { + if (url?.includes(SCHEME_SEPARATOR)) { + const startIndex = + url.indexOf(SCHEME_SEPARATOR) + SCHEME_SEPARATOR.length; + return url.substring(startIndex); + } + + return url; + }; + + return ( + + {imageProps().src ? : } + + + {title} + +
{description}
+
+ {displayUrl()} +
+
+ {onClose && as === "div" && ( + + onClose(url)} + > + + + + )} +
+ ); +}; + +LinkPreview.propTypes = { + /** Used to set the root element to either am anchor link or div container */ + as: PropTypes.oneOf(["a", "div"]), + /** The description to be displayed */ + description: PropTypes.string, + /** The config for the image to be displayed */ + image: PropTypes.shape({ + /** The url string to be passed to image src */ + url: PropTypes.string.isRequired, + /** The string to be passed to image alt */ + alt: PropTypes.string, + }), + /** Flag to trigger the loading animation */ + isLoading: PropTypes.bool, + /** The callback to handle the deleting of a Preview, to hide the close button do not set this prop */ + onClose: PropTypes.func, + /** The title to be displayed */ + title: PropTypes.string, + /** The url string to be displayed and to serve as the link's src */ + url: PropTypes.string, +}; + +LinkPreview.displayName = "LinkPreview"; + +export default LinkPreview; diff --git a/src/components/link-preview/link-preview.d.ts b/src/components/link-preview/link-preview.d.ts new file mode 100644 index 0000000000..63d6547b33 --- /dev/null +++ b/src/components/link-preview/link-preview.d.ts @@ -0,0 +1,24 @@ +interface ImageShape { + /** The url string to be passed to image src */ + url: string; + /** The string to be passed to image alt */ + alt?: string; +} + +export interface LinkPreviewProps { + description?: string; + /** The config for the image to be displayed */ + image?: ImageShape; + /** Flag to trigger the loading animation */ + isLoading?: boolean; + /** The callback to handle the deleting of a Preview, to hide the close button do not set this prop */ + onClose?: (url: string) => void; + /** The title to be displayed */ + title?: string; + /** The url string to be displayed and to serve as the link's src */ + url?: string; +} + +declare function LinkPreview(props: LinkPreviewProps): JSX.Element; + +export default LinkPreview; diff --git a/src/components/link-preview/link-preview.spec.js b/src/components/link-preview/link-preview.spec.js new file mode 100644 index 0000000000..2983c4abb7 --- /dev/null +++ b/src/components/link-preview/link-preview.spec.js @@ -0,0 +1,215 @@ +import React from "react"; +import { mount } from "enzyme"; +import LinkPreview from "./link-preview.component"; +import { + StyledLinkPreview, + StyledPreviewWrapper, + StyledCloseIconWrapper, + StyledTitle, + StyledDescription, + StyledUrl, +} from "./link-preview.style"; +import PreviewBars, { StyledPreview } from "../preview/preview.style"; +import Image from "../image"; +import Placeholder from "./__internal__/placeholder.component"; +import StyledIconButton from "../icon-button/icon-button.style"; +import { baseTheme } from "../../style/themes"; +import { assertStyleMatch } from "../../__spec_helper__/test-utils"; + +const render = (props = {}) => { + return mount(); +}; + +describe("LinkPreview", () => { + let wrapper; + + describe("styling", () => { + it("matches expected for default configuration", () => { + wrapper = render(); + + assertStyleMatch( + { + display: "flex", + margin: "8px", + border: `1px solid ${baseTheme.editorLinkPreview.border}`, + backgroundColor: baseTheme.editorLinkPreview.background, + textDecoration: "none", + color: baseTheme.text.color, + }, + wrapper.find(StyledLinkPreview) + ); + + assertStyleMatch( + { + flexGrow: "1", + padding: "16px", + }, + wrapper.find(StyledPreviewWrapper) + ); + + assertStyleMatch( + { + marginTop: "8px", + }, + wrapper.find(StyledPreviewWrapper), + { modifier: `${PreviewBars}:first-of-type` } + ); + + assertStyleMatch( + { + marginTop: "16px", + }, + wrapper.find(StyledPreviewWrapper), + { modifier: `${PreviewBars}:not(:first-of-type)` } + ); + }); + + it("matches the expected when `onClose` prop is set", () => { + wrapper = render({ onClose: () => {}, as: "div" }); + + assertStyleMatch( + { + padding: "16px", + }, + wrapper.find(StyledCloseIconWrapper) + ); + }); + + it("matches the expected when `isLoading` is false", () => { + wrapper = render({ isLoading: false, url: "foo" }); + + assertStyleMatch( + { + outline: `2px solid ${baseTheme.colors.focus}`, + }, + wrapper.find(StyledLinkPreview), + { modifier: ":focus" } + ); + + assertStyleMatch( + { + cursor: "pointer", + backgroundColor: baseTheme.editorLinkPreview.hoverBackground, + }, + wrapper.find(StyledLinkPreview), + { modifier: ":hover" } + ); + + assertStyleMatch( + { + display: "flex", + flexDirection: "column", + height: "100%", + }, + wrapper.find(StyledPreviewWrapper), + { modifier: `${StyledPreview}` } + ); + + assertStyleMatch( + { + whiteSpace: "nowrap", + textOverflow: "ellipsis", + fontWeight: "700", + fontSize: "14px", + lineHeight: "21px", + }, + wrapper.find(StyledTitle) + ); + + assertStyleMatch( + { + flexGrow: "1", + }, + wrapper.find(StyledDescription) + ); + + assertStyleMatch( + { + display: "-webkit-box", + overflow: "hidden", + textOverflow: "ellipsis", + fontWeight: "400", + fontSize: "14px", + lineHeight: "21px", + }, + wrapper.find(StyledDescription), + { modifier: "> div" } + ); + + assertStyleMatch( + { + whiteSpace: "nowrap", + textOverflow: "ellipsis", + fontWeight: "400", + fontSize: "14px", + lineHeight: "21px", + color: baseTheme.editorLinkPreview.url, + }, + wrapper.find(StyledUrl) + ); + }); + }); + + describe("image props", () => { + it("renders the placeholder Image if no config found", () => { + wrapper = render(); + expect(wrapper.find(Image).exists()).toBeFalsy(); + expect(wrapper.find(Placeholder).exists()).toBeTruthy(); + }); + + it("renders the Image with the provided config", () => { + wrapper = render({ + image: { + url: "foo", + alt: "foo image alt text", + }, + }); + expect(wrapper.find(Image).prop("src")).toEqual("foo"); + expect(wrapper.find(Image).prop("alt")).toEqual("foo image alt text"); + }); + + it("renders the Image with the default alt text if none found in config", () => { + wrapper = render({ + image: { + url: "foo", + }, + }); + expect(wrapper.find(Image).prop("src")).toEqual("foo"); + expect(wrapper.find(Image).prop("alt")).toEqual("Link preview image"); + }); + }); + + describe.each(["div", "a"])("when the as prop is set to `%s`", (as) => { + it("sets the props as expected", () => { + wrapper = render({ as, isLoading: false, url: "foo" }); + const expectedUrl = as === "div" ? undefined : "foo"; + const expectedTarget = as === "div" ? undefined : "_blank"; + const expectedTabIndex = as === "div" ? -1 : 0; + + expect(wrapper.find(StyledLinkPreview).prop("href")).toEqual(expectedUrl); + expect(wrapper.find(StyledLinkPreview).prop("target")).toEqual( + expectedTarget + ); + expect(wrapper.find(StyledLinkPreview).prop("tabIndex")).toEqual( + expectedTabIndex + ); + }); + }); + + describe("clicking the close link preview close button", () => { + it("should call the onClose callback with expected parameter", () => { + const onClose = jest.fn(); + wrapper = render({ as: "div", onClose, url: "foo" }); + wrapper.find(StyledIconButton).prop("onClick")(); + + expect(onClose).toHaveBeenCalledWith("foo"); + }); + }); + + describe("displaying the url", () => { + it("trims the scheme", () => { + wrapper = render({ url: "https://www.foo.com" }); + expect(wrapper.find(StyledUrl).prop("children")).toEqual("www.foo.com"); + }); + }); +}); diff --git a/src/components/link-preview/link-preview.stories.js b/src/components/link-preview/link-preview.stories.js new file mode 100644 index 0000000000..2b3d56b3b6 --- /dev/null +++ b/src/components/link-preview/link-preview.stories.js @@ -0,0 +1,31 @@ +import React from "react"; +import { action } from "@storybook/addon-actions"; +import LinkPreview from "./link-preview.component"; + +export default { + title: "Design System/Link Preview/Test", + component: LinkPreview, + parameters: { + info: { + disable: true, + }, + knobs: { escapeHTML: false }, + chromatic: { + disable: true, + }, + }, +}; + +export const Default = () => ( + action("close icon clicked")(url)} + title="This is an example of a title" + url="https://www.sage.com" + description="Captain, why are we out here chasing comets? I'd like to think that I haven't changed those things, sir. Computer, lights up! Not if I weaken first. Damage report! Yesterday I did not know how to eat gagh. The Federation's gone; the Borg is everywhere! We know you're dealing in stolen ore. But I wanna talk about the assassination attempt on Lieutenant Worf. Our neural pathways have become accustomed to your sensory input patterns. Wouldn't that bring about chaos?" + /> +); + +Default.story = { + name: "default", +}; diff --git a/src/components/link-preview/link-preview.stories.mdx b/src/components/link-preview/link-preview.stories.mdx new file mode 100644 index 0000000000..4b6b5589cf --- /dev/null +++ b/src/components/link-preview/link-preview.stories.mdx @@ -0,0 +1,70 @@ +import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks'; + +import LinkPreview from '.'; + + + +# LinkPreview + +## Contents +- [Quick Start](#quick-start) +- [Examples](#examples) +- [Props](#props) + +## Quick Start +```javascript +import LinkPreview from "carbon-react/lib/components/link-preview"; +``` + +## Examples + +### Default +By default the component will render an anchor element if it is not in a loading state. This means the whole component can +be focused via tabbing and pressing enter or clicking will open the target in a new tab. The component surfaces a range of +props which can be seen in the table below. If no `image` props are passed a placeholder will be used. + + + + + + + +### With loading state +If no `url` is passed or the `isLoading` prop is true the component will display in a loading state. When in this state, it +renders as a `div` element: it cannot be focused via tabbing and clicking will not trigger anything. + + + + + + + +### With close icon +If the component is rendered in edit mode as a `div` element, it is possible to pass an `onClose` callback which will render +a close icon and pass the callback to it. The icon can be focused via tabbing and the callback will be triggered via click or +via pressing enter or space when focused. + + + + console.log("close icon clicked")} + title="This is an example of a title" + url="https://www.sage.com" + description="Captain, why are we out here chasing comets? I'd like to think that I haven't changed those things, sir. Computer, lights up! Not if I weaken first. Damage report! Yesterday I did not know how to eat gagh. The Federation's gone; the Borg is everywhere! We know you're dealing in stolen ore. But I wanna talk about the assassination attempt on Lieutenant Worf. Our neural pathways have become accustomed to your sensory input patterns. Wouldn't that bring about chaos?" + /> + + + +## Props + +### LinkPreview + + diff --git a/src/components/link-preview/link-preview.style.js b/src/components/link-preview/link-preview.style.js new file mode 100644 index 0000000000..83ac10c791 --- /dev/null +++ b/src/components/link-preview/link-preview.style.js @@ -0,0 +1,106 @@ +import styled, { css } from "styled-components"; +import { baseTheme } from "../../style/themes"; +import PreviewBars, { StyledPreview } from "../preview/preview.style"; + +const StyledLinkPreview = styled.a` + display: flex; + margin: 8px; + text-decoration: none; + outline: none; + + ${({ theme, as }) => css` + border: 1px solid ${theme.editorLinkPreview.border}; + background-color: ${theme.editorLinkPreview.background}; + color: ${theme.text.color}; + + ${as !== "div" && + css` + :focus { + outline: 2px solid ${theme.colors.focus}; + outline-offset: -1px; + } + + :hover { + cursor: pointer; + background-color: ${theme.editorLinkPreview.hoverBackground}; + } + `} + `} +`; + +const StyledCloseIconWrapper = styled.div` + padding: 16px; +`; + +const StyledPreviewWrapper = styled.div` + flex-grow: 1; + padding: 16px; + + ${({ isLoading }) => + !isLoading && + css` + ${StyledPreview} { + display: flex; + flex-direction: column; + height: 100%; + } + `} + + ${PreviewBars}:first-of-type { + margin-top: 8px; + } + + ${PreviewBars}:not(:first-of-type) { + margin-top: 16px; + } +`; + +const StyledTitle = styled.div` + white-space: nowrap; + text-overflow: ellipsis; + font-weight: 700; + font-size: 14px; + line-height: 21px; +`; + +const StyledDescription = styled.div` + > div { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 400; + font-size: 14px; + line-height: 21px; + padding-top: 4px; + } + + flex-grow: 1; +`; + +const StyledUrl = styled.div` + white-space: nowrap; + text-overflow: ellipsis; + font-weight: 400; + font-size: 14px; + line-height: 21px; + color: ${({ theme }) => theme.editorLinkPreview.url}; +`; + +StyledLinkPreview.defaultProps = { + theme: baseTheme, +}; + +StyledUrl.defaultProps = { + theme: baseTheme, +}; + +export { + StyledLinkPreview, + StyledCloseIconWrapper, + StyledPreviewWrapper, + StyledTitle, + StyledDescription, + StyledUrl, +}; diff --git a/src/components/text-editor/__internal__/editor-link/editor-link.component.js b/src/components/text-editor/__internal__/editor-link/editor-link.component.js index adaddff667..25c41781d3 100644 --- a/src/components/text-editor/__internal__/editor-link/editor-link.component.js +++ b/src/components/text-editor/__internal__/editor-link/editor-link.component.js @@ -25,6 +25,7 @@ const EditorLink = ({ children, contentState, entityKey, ...rest }) => { target="_blank" rel="noopener noreferrer" tabbable={false} + type="LINK" {...rest} > {children} diff --git a/src/style/themes/base/base-theme.config.js b/src/style/themes/base/base-theme.config.js index 9005f2dc07..1ea8a2cd47 100644 --- a/src/style/themes/base/base-theme.config.js +++ b/src/style/themes/base/base-theme.config.js @@ -362,6 +362,13 @@ export default (palette) => { timeStamp: "rgba(0,0,0,0.65)", }, + editorLinkPreview: { + background: palette.slateTint(95), + hoverBackground: palette.slateTint(80), + border: palette.slateTint(90), + url: palette.slateTint(10), + }, + zIndex: { smallOverlay: 10, overlay: 1000, From 5779ad094d1d024eb09279939a035bd96b8ca6a6 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Thu, 15 Jul 2021 16:34:55 +0100 Subject: [PATCH 3/5] test(link-preview): add cypress tests for component and install new realEvent dependency Adds tests for the functionality of the `LinkPreview` which also required the installing of `cypress-real-events` dependency to achieve it --- .../designSystem/linkPreview.feature | 39 ++++++++++++ cypress/locators/link-preview/index.js | 7 +++ cypress/support/index.js | 1 + .../step-definitions/link-preview-steps.js | 62 +++++++++++++++++++ cypress/tsconfig.json | 2 +- package-lock.json | 5 ++ package.json | 1 + 7 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 cypress/features/regression/designSystem/linkPreview.feature create mode 100644 cypress/locators/link-preview/index.js create mode 100644 cypress/support/step-definitions/link-preview-steps.js diff --git a/cypress/features/regression/designSystem/linkPreview.feature b/cypress/features/regression/designSystem/linkPreview.feature new file mode 100644 index 0000000000..426fb22cf7 --- /dev/null +++ b/cypress/features/regression/designSystem/linkPreview.feature @@ -0,0 +1,39 @@ +Feature: Design System Link Preview component + I want to test Design System Link Preview component + + @positive + Scenario: Verify hover color of Link Preview component + Given I open "Design System Link Preview" component page "default story" in no iframe + When I hover mouse onto Link Preview component + Then Link Preview text element has correct background-color "rgb(204, 214, 219)" + + @positive + Scenario: Verify border outline color and width of Link Preview on focus + Given I open "Design System Link Preview" component page "default story" in no iframe + When I focus Link Preview component + Then Link Preview has the border outline color "rgb(255, 181, 0)" and width "2px" + + @positive + Scenario: Verify border outline color and width of close icon on focus + Given I open "Design System Link Preview" component page "with close icon" in no iframe + When I focus Link Preview close icon + Then Link Preview close icon has the border outline color "rgb(255, 181, 0)" and width "3px" + + @positive + Scenario: Check the delete event using the mouse + Given I open "Design System Link Preview Test" component page "default" + And clear all actions in Actions Tab + When I click Link Preview close icon in Iframe + Then "close icon clicked: \"https://www.sage.com\"" action is called in Actions Tab for Link Preview + + @positive + Scenario Outline: Check the delete event using key + Given I open "Design System Link Preview Test" component page "default" + And clear all actions in Actions Tab + And I focus Link Preview close icon in Iframe + When I click onto Link Preview close icon using "" key + Then "close icon clicked: \"https://www.sage.com\"" action is called in Actions Tab for Link Preview + Examples: + | key | + | Enter | + | Space | \ No newline at end of file diff --git a/cypress/locators/link-preview/index.js b/cypress/locators/link-preview/index.js new file mode 100644 index 0000000000..b9b6932113 --- /dev/null +++ b/cypress/locators/link-preview/index.js @@ -0,0 +1,7 @@ +import { DLS_ROOT } from "../locators"; +import { PILL_CLOSE_ICON } from "../pill/locators"; + +// component preview locators +export const linkPreviewText = () => cy.get(DLS_ROOT).find("a"); +export const linkPreviewCloseIcon = () => cy.get(PILL_CLOSE_ICON); +export const linkPreviewCloseIconIframe = () => cy.iFrame(PILL_CLOSE_ICON); diff --git a/cypress/support/index.js b/cypress/support/index.js index ce6db634f6..5730b48f3c 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,4 +1,5 @@ import "cypress-axe"; +import "cypress-real-events/support"; require("cypress-plugin-tab"); diff --git a/cypress/support/step-definitions/link-preview-steps.js b/cypress/support/step-definitions/link-preview-steps.js new file mode 100644 index 0000000000..f85c3ea781 --- /dev/null +++ b/cypress/support/step-definitions/link-preview-steps.js @@ -0,0 +1,62 @@ +import { eventInAction } from "../../locators"; +import { + linkPreviewText, + linkPreviewCloseIcon, + linkPreviewCloseIconIframe, +} from "../../locators/link-preview"; +import { keyCode } from "../helper"; + +When("I hover mouse onto Link Preview component", () => { + linkPreviewText().realHover(); +}); + +Then("Link Preview text element has correct background-color {string}", () => { + linkPreviewText().should( + "have.css", + "background-color", + "rgb(204, 214, 219)" + ); +}); + +When("I focus Link Preview component", () => { + linkPreviewText().focus(); +}); + +Then( + "Link Preview has the border outline color {string} and width {string}", + (color, width) => { + linkPreviewText() + .should("have.css", "outline-color", color) + .and("have.css", "outline-width", width); + } +); + +When("I focus Link Preview close icon", () => { + linkPreviewCloseIcon().parent().focus(); +}); + +Then( + "Link Preview close icon has the border outline color {string} and width {string}", + (color, width) => { + linkPreviewCloseIcon() + .parent() + .should("have.css", "outline-color", color) + .and("have.css", "outline-width", width); + } +); + +When("I click Link Preview close icon in Iframe", () => { + linkPreviewCloseIconIframe().click(); +}); + +When("I focus Link Preview close icon in Iframe", () => { + linkPreviewCloseIconIframe().parent().focus(); +}); + +When("I click onto Link Preview close icon using {string} key", (key) => { + linkPreviewCloseIconIframe().trigger("keydown", keyCode(key)); +}); + +Then("{string} action is called in Actions Tab for Link Preview", (event) => { + eventInAction(event); +}); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 11a0a01afe..667c37b271 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -4,7 +4,7 @@ "baseUrl": "../node_modules", "outDir": "dist", "types": [ - "cypress" + "cypress", "cypress-real-events" ] }, "include": [ diff --git a/package-lock.json b/package-lock.json index 0311138409..e074ea4f34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11790,6 +11790,11 @@ "ally.js": "^1.4.1" } }, + "cypress-real-events": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.5.0.tgz", + "integrity": "sha512-5virdYmZmtnpVqX4U0Ht66985b5MpCHE6icDXReQGognja5d2FuRD+Hh2MqzJqOTmai74z50u5+nmTZ/Dn/VcQ==" + }, "cz-conventional-changelog": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", diff --git a/package.json b/package.json index 63696e95ee..bfa8135fea 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "bowser": "~1.5.0", "classnames": "~2.2.6", "crypto-js": "~3.3.0", + "cypress-real-events": "^1.5.0", "escape-string-regexp": "^4.0.0", "immutable": "~3.8.2", "invariant": "^2.2.4", From a436ea5b2ee05e4d54afa2a930815aedf4673e84 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Thu, 17 Jun 2021 11:04:37 +0100 Subject: [PATCH 4/5] feat(text-editor): add support for rendering editor link previews Adds `previews` and `onLinkAdded` props to `TextEditor` to support the rendering of `LinkPreviews` for any urls that have associated meta data. The `onLinkAdded` callback is integrated with a context to allow `EditorLink` components to report when they have been rendered via the decorator. --- .../editor-link/editor-link.component.js | 15 +- .../editor-link/editor-link.spec.js | 53 ++++++- .../text-editor/text-editor.component.js | 132 +++++++++++------- src/components/text-editor/text-editor.d.ts | 4 + .../text-editor/text-editor.spec.js | 68 +++++++++ .../text-editor/text-editor.stories.mdx | 68 +++++++++ .../text-editor/text-editor.style.js | 7 +- 7 files changed, 287 insertions(+), 60 deletions(-) diff --git a/src/components/text-editor/__internal__/editor-link/editor-link.component.js b/src/components/text-editor/__internal__/editor-link/editor-link.component.js index 25c41781d3..becaebe6cc 100644 --- a/src/components/text-editor/__internal__/editor-link/editor-link.component.js +++ b/src/components/text-editor/__internal__/editor-link/editor-link.component.js @@ -1,6 +1,7 @@ -import React from "react"; +import React, { useContext, useEffect } from "react"; import PropTypes from "prop-types"; import StyledLink from "./editor-link.style"; +import { EditorContext } from "../../text-editor.component"; const EditorLink = ({ children, contentState, entityKey, ...rest }) => { const url = @@ -17,6 +18,15 @@ const EditorLink = ({ children, contentState, entityKey, ...rest }) => { const validUrl = buildValidUrl(); + const { onLinkAdded, editMode } = useContext(EditorContext); + + useEffect(() => { + if (onLinkAdded) { + onLinkAdded(validUrl); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [validUrl]); + return ( { aria-label={validUrl} target="_blank" rel="noopener noreferrer" - tabbable={false} - type="LINK" + tabbable={!editMode} {...rest} > {children} diff --git a/src/components/text-editor/__internal__/editor-link/editor-link.spec.js b/src/components/text-editor/__internal__/editor-link/editor-link.spec.js index e47a28d24f..41ccbae9f2 100644 --- a/src/components/text-editor/__internal__/editor-link/editor-link.spec.js +++ b/src/components/text-editor/__internal__/editor-link/editor-link.spec.js @@ -2,9 +2,16 @@ import React from "react"; import { mount } from "enzyme"; import EditorLink from "./editor-link.component"; import Link from "../../../link"; +import { EditorContext } from "../../text-editor.component"; -const render = (props = {}, renderer = mount) => { - return renderer(); +const onLinkAdded = jest.fn(); + +const render = (props = {}, editMode = true) => { + return mount( + + + + ); }; describe("EditorLink", () => { @@ -72,4 +79,46 @@ describe("EditorLink", () => { expect(wrapper.find(Link).props().href).toEqual("http://foo"); expect(wrapper.find(Link).text()).toEqual("foo"); }); + + it("calls the `onLinkAdded` callback with the validUrl", () => { + render({ + entityKey: "bar", + children: [ +
+ foo +
, + ], + }); + + expect(onLinkAdded).toHaveBeenCalledWith("http://foo"); + }); + + it("prevents link from being tabbable if in editMode", () => { + const wrapper = render({ + entityKey: "bar", + children: [ +
+ foo +
, + ], + }); + + expect(wrapper.find(Link).props().tabbable).toEqual(false); + }); + + it("allows link to be tabbable if not in editMode", () => { + const wrapper = render( + { + entityKey: "bar", + children: [ +
+ foo +
, + ], + }, + false + ); + + expect(wrapper.find(Link).props().tabbable).toEqual(true); + }); }); diff --git a/src/components/text-editor/text-editor.component.js b/src/components/text-editor/text-editor.component.js index db25dc3fa1..ed33d5ea39 100644 --- a/src/components/text-editor/text-editor.component.js +++ b/src/components/text-editor/text-editor.component.js @@ -44,6 +44,8 @@ const NUMBERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; const INLINE_STYLES = ["BOLD", "ITALIC"]; const BLOCK_TYPES = ["unordered-list-item", "ordered-list-item"]; +export const EditorContext = React.createContext({}); + const TextEditor = React.forwardRef( ( { @@ -57,6 +59,8 @@ const TextEditor = React.forwardRef( info, toolbarElements, rows, + previews, + onLinkAdded, ...rest }, ref @@ -161,8 +165,8 @@ const TextEditor = React.forwardRef( ) { onChange(resetBlockType(value, newBlockType)); setActiveInlines({ - BOLD: hasInlineStyle(value, "BOLD"), - ITALIC: hasInlineStyle(value, "ITALIC"), + BOLD: hasInlineStyle(value, INLINE_STYLES[0]), + ITALIC: hasInlineStyle(value, INLINE_STYLES[1]), }); return true; @@ -175,7 +179,7 @@ const TextEditor = React.forwardRef( const handlePastedText = (pastedText) => { const selectedTextLength = getSelectedLength(value); - const newLength = contentLength + pastedText.length - selectedTextLength; + const newLength = contentLength + pastedText?.length - selectedTextLength; // if the pastedText will exceed the limit trim the excess if (newLength > characterLimit) { const newContentState = Modifier.insertText( @@ -193,6 +197,7 @@ const TextEditor = React.forwardRef( return "handled"; } + setActiveInlines({}); return "not-handled"; @@ -252,10 +257,10 @@ const TextEditor = React.forwardRef( setInlines([...inlines, style]); }; - const handleBlockStyleChange = (ev, blockType) => { + const handleBlockStyleChange = (ev, newBlockType) => { ev.preventDefault(); handleEditorFocus(true); - onChange(RichUtils.toggleBlockType(value, blockType)); + onChange(RichUtils.toggleBlockType(value, newBlockType)); const temp = []; INLINE_STYLES.forEach((style) => { if (activeInlines[style] !== undefined) { @@ -294,56 +299,73 @@ const TextEditor = React.forwardRef( value, ]); + const handlePreviewClose = (onClose, url) => { + onClose(url); + editor.current.focus(); + }; + return ( - - handleEditorFocus(true)}> - - - - - - handleEditorFocus(true)} - onBlur={() => handleEditorFocus(false)} - editorState={editorState} - onChange={onChange} - handleBeforeInput={handleBeforeInput} - handlePastedText={handlePastedText} - handleKeyCommand={handleKeyCommand} + + + handleEditorFocus(true)}> + + + + - - handleBlockStyleChange(ev, blockType) - } - setInlineStyle={(ev, inlineStyle, keyboardUsed) => - handleInlineStyleChange(ev, inlineStyle, keyboardUsed) - } - editorState={editorState} - activeControls={activeControls} - canFocus={focusToolbar} - toolbarElements={toolbarElements} - /> - - - + hasError={!!error} + rows={rows} + hasPreview={!!React.Children.count(previews)} + > + + handleEditorFocus(true)} + onBlur={() => handleEditorFocus(false)} + editorState={editorState} + onChange={onChange} + handleBeforeInput={handleBeforeInput} + handlePastedText={handlePastedText} + handleKeyCommand={handleKeyCommand} + ariaLabelledBy={labelId.current} + ariaDescribedBy={labelId.current} + blockStyleFn={blockStyleFn} + keyBindingFn={keyBindingFn} + /> + {React.Children.map(previews, (preview) => { + const { onClose } = preview.props; + return React.cloneElement(preview, { + as: "div", + onClose: onClose + ? (url) => handlePreviewClose(onClose, url) + : undefined, + }); + })} + + handleBlockStyleChange(ev, newBlockType) + } + setInlineStyle={(ev, inlineStyle, keyboardUsed) => + handleInlineStyleChange(ev, inlineStyle, keyboardUsed) + } + editorState={editorState} + activeControls={activeControls} + canFocus={focusToolbar} + toolbarElements={toolbarElements} + /> + + + + ); } ); @@ -381,6 +403,10 @@ TextEditor.propTypes = { return null; }, + /** The previews to display of any links added to the Editor */ + previews: PropTypes.arrayOf(PropTypes.node), + /** Callback to report a url when a link is added */ + onLinkAdded: PropTypes.func, }; export const TextEditorState = EditorState; diff --git a/src/components/text-editor/text-editor.d.ts b/src/components/text-editor/text-editor.d.ts index 07578ed66d..8a4843bc9d 100644 --- a/src/components/text-editor/text-editor.d.ts +++ b/src/components/text-editor/text-editor.d.ts @@ -23,6 +23,10 @@ export interface TextEditorProps extends MarginProps { info?: string; /** Number greater than 2 multiplied by line-height (21px) to override the default min-height of the editor */ rows?: number; + /** The previews to display of any links added to the Editor */ + previews?: React.ReactNode[]; + /** Callback to report a url when a link is added */ + onLinkAdded?: (url: string) => void; } declare function TextEditor(props: TextEditorProps & React.RefAttributes): JSX.Element; diff --git a/src/components/text-editor/text-editor.spec.js b/src/components/text-editor/text-editor.spec.js index 3a4ee4e8f9..b2c94865ba 100644 --- a/src/components/text-editor/text-editor.spec.js +++ b/src/components/text-editor/text-editor.spec.js @@ -25,7 +25,9 @@ import guid from "../../utils/helpers/guid/guid"; import Label from "../../__experimental__/components/label"; import LabelWrapper from "./__internal__/label-wrapper"; import ValidationIcon from "../validations"; +import EditorLinkPreview from "../link-preview"; import { isSafari } from "../../utils/helpers/browser-type-check"; +import IconButton from "../icon-button"; jest.mock("../../utils/helpers/browser-type-check"); isSafari.mockImplementation(() => false); @@ -935,6 +937,72 @@ describe("TextEditor", () => { }); }); + describe("Link previews", () => { + const onLinkAdded = jest.fn(); + + it("reports the url when a valid one is added and enter is pressed", () => { + const url = "http://foo.com"; + wrapper = render({ onLinkAdded }); + + act(() => { + wrapper + .find(TextEditor) + .props() + .onChange(addToEditorState(url, wrapper.find(Editor).props())); + }); + act(() => { + wrapper.update(); + }); + act(() => { + wrapper.find(Editor).props().handleKeyCommand("split-block"); + }); + + expect(onLinkAdded).toHaveBeenCalledWith(url); + }); + + it("reports the url when a valid one is inputted and space is pressed", () => { + const url = "http://foo.com"; + wrapper = render({ onLinkAdded }); + + act(() => { + wrapper + .find(TextEditor) + .props() + .onChange(addToEditorState(url, wrapper.find(Editor).props())); + }); + act(() => { + wrapper.update(); + }); + act(() => { + wrapper.find(Editor).props().handleBeforeInput(" "); + }); + + expect(onLinkAdded).toHaveBeenCalledWith(url); + }); + + it("renders any EditorLinkPreviews passed in via the `previews` prop", () => { + const previews = [ + , + , + , + ]; + wrapper = render({ onLinkAdded, previews }); + expect(wrapper.find(EditorLinkPreview).length).toEqual(3); + }); + + it("calls the onClose callback if one is passed when the close icon is clicked", () => { + const onClose = jest.fn(); + const previews = [ + , + , + , + ]; + wrapper = render({ onLinkAdded, previews }); + wrapper.find(EditorLinkPreview).find(IconButton).simulate("click"); + expect(onClose).toHaveBeenCalledWith("foo"); + }); + }); + afterAll(() => { // Clear Mock window.scrollTo.mockRestore(); diff --git a/src/components/text-editor/text-editor.stories.mdx b/src/components/text-editor/text-editor.stories.mdx index 8e9f2b839f..17da597f89 100644 --- a/src/components/text-editor/text-editor.stories.mdx +++ b/src/components/text-editor/text-editor.stories.mdx @@ -5,6 +5,7 @@ import TextEditor, { TextEditorContentState as ContentState, } from "./text-editor.component"; import Button from "../button"; +import EditorLinkPreview from "../link-preview"; import StyledSystemProps from '../../../.storybook/utils/styled-system-props'; +### With link previews +It is possible to render `EditorLinkPreview`s via the `previews` prop. The `onLinkAdded` prop provides a callback +that will allow any link added to report the url back to be used to make a call to whatever service or api you want. +Whilst in the `Editor`, these previews can be deleted by clicking or pressing the enter key, when focused, on the close +icon. This example has mocked some functionality: previews will display for any link that has a url ending in `.com`, +`.co.uk`, `.org` or `.net`. See the prop table below for the available props for the `EditorLinkPreview` component. + + + + {() => { + const [value, setValue] = useState( + EditorState.createWithContent( + ContentState.createFromText("www.sage.com") + ) + ); + const previews = useRef([ + removeUrl(urlString)} + title="This is an example of a title" + url="https://www.sage.com" + description="Captain, why are we out here chasing comets? I'd like to think that I haven't changed those things, sir. Computer, lights up! Not if I weaken first. Damage report! Yesterday I did not know how to eat gagh. The Federation's gone; the Borg is everywhere! We know you're dealing in stolen ore. But I wanna talk about the assassination attempt on Lieutenant Worf. Our neural pathways have become accustomed to your sensory input patterns. Wouldn't that bring about chaos?" + /> + ]); + const removeUrl = (reportedUrl) => { + previews.current = previews.current.filter((preview) => reportedUrl !== preview.props.url) + }; + const checkValidDomain = (url) => { + const domainsWhitelist = [".com", ".co.uk", ".org", ".net"]; + const result = domainsWhitelist.filter(domain => url.endsWith(domain)).length; + return !!result; + } + const addUrl = (reportedUrl) => { + if (!previews.current.filter((preview) => reportedUrl === preview.props.url).length && checkValidDomain(reportedUrl)) { + const previewConfig = { + title: "This is an example of a title", + isLoading: false, + url: reportedUrl, + image: undefined, + description: "Captain, why are we out here chasing comets? I'd like to think that I haven't changed those things, sir. Computer, lights up! Not if I weaken first. Damage report! Yesterday I did not know how to eat gagh. The Federation's gone; the Borg is everywhere! We know you're dealing in stolen ore. But I wanna talk about the assassination attempt on Lieutenant Worf. Our neural pathways have become accustomed to your sensory input patterns. Wouldn't that bring about chaos?" + }; + const preview = ( + removeUrl(urlString)} + {...previewConfig} + /> + ); + previews.current.push(preview) + } + }; + return ( +
+ { + setValue(newValue); + }} + value={value} + labelText="Text Editor Label" + required + previews={previews.current} + onLinkAdded={addUrl} + /> +
+ ); + }} +
+
+ ## Props ### Text Editor diff --git a/src/components/text-editor/text-editor.style.js b/src/components/text-editor/text-editor.style.js index db322dd1d8..4ba4bd1f81 100644 --- a/src/components/text-editor/text-editor.style.js +++ b/src/components/text-editor/text-editor.style.js @@ -14,9 +14,12 @@ StyledEditorWrapper.defaultProps = { }; const StyledEditorContainer = styled.div` - ${({ theme, hasError, rows }) => css` - min-height: ${rows ? `${rows * lineHeight}` : "220"}px; + ${({ theme, hasError, rows, hasPreview }) => css` + min-height: ${rows + ? `${rows * lineHeight}` + : `${hasPreview ? 125 : 220}`}px; min-width: 320px; + position: relative; div.DraftEditor-root { min-height: inherit; From 5519c3045cf3f81bc18f4e426e19c3d01a021201 Mon Sep 17 00:00:00 2001 From: Ed Leeks Date: Tue, 22 Jun 2021 17:15:09 +0100 Subject: [PATCH 5/5] feat(note): add support for rendering link previews Adds `preview` prop to support rendering `LinkPreview`s beneath the note content --- .../accessibilityDesignSystem.feature | 2 +- src/components/note/index.d.ts | 4 +- src/components/note/note.component.js | 68 +++++++++-------- src/components/note/note.d.ts | 4 + src/components/note/note.spec.js | 18 ++++- src/components/note/note.stories.mdx | 76 ++++++++++++++++--- src/components/note/note.style.js | 18 ++++- 7 files changed, 140 insertions(+), 50 deletions(-) diff --git a/cypress/features/regression/accessibility/accessibilityDesignSystem.feature b/cypress/features/regression/accessibility/accessibilityDesignSystem.feature index 5cb2718784..81621bfc4e 100644 --- a/cypress/features/regression/accessibility/accessibilityDesignSystem.feature +++ b/cypress/features/regression/accessibility/accessibilityDesignSystem.feature @@ -161,7 +161,7 @@ Feature: Accessibility tests - Design System folder @accessibility Scenario: Design System Note component - When I open "Design System Note" component page "inline controls" in no iframe + When I open "Design System Note" component page "with inline controls" in no iframe Then "Note inline controls" component has no accessibility violations @accessibility diff --git a/src/components/note/index.d.ts b/src/components/note/index.d.ts index cf9d3a5f05..b9eb4b1e3e 100644 --- a/src/components/note/index.d.ts +++ b/src/components/note/index.d.ts @@ -1,2 +1,2 @@ -export { default } from './note'; -export * from './note'; +export { default } from "./note"; +export * from "./note"; diff --git a/src/components/note/note.component.js b/src/components/note/note.component.js index cd3f6824ca..0f5a81b0e2 100644 --- a/src/components/note/note.component.js +++ b/src/components/note/note.component.js @@ -15,6 +15,7 @@ import StatusWithTooltip from "./__internal__/status-with-tooltip"; import { ActionPopover } from "../action-popover"; import { filterStyledSystemMarginProps } from "../../style/utils"; import { getDecoratedValue } from "../text-editor/__internal__/utils"; +import { EditorContext } from "../text-editor/text-editor.component"; const marginPropTypes = filterStyledSystemMarginProps( styledSystemPropTypes.space @@ -28,6 +29,8 @@ const Note = ({ name, createdDate, status, + previews, + onLinkAdded, ...rest }) => { invariant(width > 0, " width must be greater than 0"); @@ -48,49 +51,44 @@ const Note = ({ const { text, timeStamp } = status; return ( - + {text} ); }; return ( - - {title && {title}} + + + {title && {title}} - {inlineControl && ( - {inlineControl} - )} + {inlineControl && ( + {inlineControl} + )} - - - - - {createdDate && ( - - {name && ( - - {name} - - )} - - {createdDate} - - {renderStatus()} - + - )} - + {React.Children.map(previews, (preview) => + React.cloneElement(preview, { as: "a", onClose: undefined }) + )} + {createdDate && ( + + + {name && ( + + {name} + + )} + + {createdDate} + + {renderStatus()} + + + )} + + ); }; @@ -113,6 +111,10 @@ Note.propTypes = { text: PropTypes.string.isRequired, timeStamp: PropTypes.string.isRequired, }), + /** The previews to display of any links added to the Editor */ + previews: PropTypes.arrayOf(PropTypes.node), + /** Callback to report a url when a link is added */ + onLinkAdded: PropTypes.func, }; export default Note; diff --git a/src/components/note/note.d.ts b/src/components/note/note.d.ts index 9fab32e801..3d297746b7 100644 --- a/src/components/note/note.d.ts +++ b/src/components/note/note.d.ts @@ -19,6 +19,10 @@ export interface NoteProps extends MarginProps { text: string; timeStamp: string; }; + /** The previews to display of any links added to the Editor */ + previews?: React.ReactNode[]; + /** Callback to report a url when a link is added */ + onLinkAdded?: (url: string) => void; } declare function Note(props: NoteProps): JSX.Element; diff --git a/src/components/note/note.spec.js b/src/components/note/note.spec.js index a85a4b6afa..f472e7e898 100644 --- a/src/components/note/note.spec.js +++ b/src/components/note/note.spec.js @@ -13,6 +13,7 @@ import { } from "./note.style"; import { ActionPopover, ActionPopoverItem } from "../action-popover"; import StyledStatusIconWrapper from "./__internal__/status-with-tooltip/status.style"; +import LinkPreview from "../link-preview"; import { assertStyleMatch, testStyledSystemMargin, @@ -83,7 +84,7 @@ describe("Note", () => { borderTop: `solid 1px ${baseTheme.tile.separator}`, }, content, - { modifier: `+ ${StyledNoteContent}` } + { modifier: ":last-of-type:not(:first-of-type)" } ); }); @@ -304,4 +305,19 @@ describe("Note", () => { }).toThrow(" inlineControl must be an instance of "); }); }); + + describe("Link Previews", () => { + it("renders any LinkPreviews passed in via the `previews` prop", () => { + const previews = [ + , + , + , + ]; + const wrapper = render({ previews }); + expect(wrapper.find(LinkPreview).length).toEqual(3); + wrapper + .find(LinkPreview) + .forEach((preview) => expect(preview.find("a").exists()).toBeTruthy()); + }); + }); }); diff --git a/src/components/note/note.stories.mdx b/src/components/note/note.stories.mdx index e8893f8c2f..07c9ff0914 100644 --- a/src/components/note/note.stories.mdx +++ b/src/components/note/note.stories.mdx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks"; +import { Meta, Story, Preview } from "@storybook/addon-docs/blocks"; import StyledSystemProps from "../../../.storybook/utils/styled-system-props"; import Note from './note.component'; import { @@ -7,7 +7,8 @@ import { ActionPopoverDivider, ActionPopoverItem } from '../action-popover'; -import { EditorState, ContentState, convertFromHTML } from 'draft-js'; +import { EditorState, ContentState, convertFromHTML, convertFromRaw } from 'draft-js'; +import LinkPreview from "../link-preview"; @@ -88,7 +89,7 @@ static method. An optional title can be provided using the `title` prop. - + {() => { const html = `

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

  • unordered
@@ -119,7 +120,7 @@ An optional title can be provided using the `title` prop. Optional inline controls can be provided using the `inlineControl` prop. This should be an `ActionPopover`. - + {() => { const html = `

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

  • unordered
@@ -162,7 +163,7 @@ Optional inline controls can be provided using the `inlineControl` prop. This sh An optional status can be provided using the `status` prop. - + {() => { const html = `

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

  • unordered
@@ -177,7 +178,7 @@ An optional status can be provided using the `status` prop. const noteContent = EditorState.createWithContent(content); const inlineControl = ( - {}}> + {} }> Edit @@ -202,6 +203,58 @@ An optional status can be provided using the `status` prop.
+### With previews +It is possible to supply link previews to the `Note` component by passing them in via the `previews` prop. Previews are +rendered as anchor elements and will behave as links, opening the page in a new tab when they are clicked or when focused +and the enter key is pressed. Similarly to the `TextEditor` component, a `onLinkAdded` prop is surfaced to allow for a link +preview's url to be reported and calls to an api can be made here as well if needed. + + + + {() => { + const json = JSON.stringify({"blocks":[{"key":"47lv5","text":"www.bbc.co.uk","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"ab5do","text":"www.sage.com","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}) + const content = convertFromRaw(JSON.parse(json)); + const noteContent = EditorState.createWithContent(content); + const inlineControl = ( + + {} }> + Edit + + + {} }> + Delete + + + ); + const previews = [ + , + + ]; + return ( +
+ +
+ ); + }} +
+
+ ### With margin Margins can be applied to the `note` component using styled-system. To see a full list of available margin props, please visit the props table at the bottom of this page. @@ -214,11 +267,11 @@ the props table at the bottom of this page. const noteContent = EditorState.createWithContent(ContentState.createFromText('Here is some plain text content')); return (
- - - - - + + + + +
); }} @@ -228,4 +281,5 @@ the props table at the bottom of this page. ## Props ### Note + diff --git a/src/components/note/note.style.js b/src/components/note/note.style.js index ddf8e3af4b..e1e21c1561 100644 --- a/src/components/note/note.style.js +++ b/src/components/note/note.style.js @@ -2,12 +2,13 @@ import styled, { css } from "styled-components"; import PropTypes from "prop-types"; import { margin } from "styled-system"; import baseTheme from "../../style/themes/base"; +import { StyledLinkPreview } from "../link-preview/link-preview.style"; const StyledNoteContent = styled.div` position: relative; width: 100%; - ${({ theme }) => ` + ${({ theme, hasPreview }) => css` &:not(:last-of-type) { padding-bottom: 24px; } @@ -25,9 +26,14 @@ const StyledNoteContent = styled.div` line-height: 21px; } - & + & { + &:last-of-type:not(:first-of-type) { border-top: solid 1px ${theme.tile.separator}; } + + ${hasPreview && + ` + margin-top: ${2 * theme.spacing}px; + `} `} `; @@ -113,6 +119,14 @@ const StyledNote = styled.div` } `} + ${StyledLinkPreview} { + margin: 0px; + + :not(:first-of-type) { + margin-top: 8px; + } + } + ${margin} `;