From bd071fe3347317ccfb0c4262e10566ad31443869 Mon Sep 17 00:00:00 2001 From: nuria1110 Date: Wed, 28 Aug 2024 14:36:22 +0100 Subject: [PATCH] feat(preview): add `shape` and `disableAnimation` props Adds `shape` prop which allows consumers to set the shape of a Preview to "text", "rectangle", "rectangle-round" and "circle". These shape variations have default dimensions however the existing `width` and `height` props can be used to change these. The "circle" shape will use the set `height` as its diameter, ignoring the `width`. The `lines` prop can still be used to render the specified amount of Previews. Also adds the `disableAnimation` prop to disable the loading animation, this can be set manually or will automatically be true when prefer reduce-motion is enabled. --- .../link-preview/link-preview.style.ts | 6 +- .../preview-placeholder.component.tsx | 34 ------- .../__internal__/preview-placeholder.style.ts | 28 ------ .../preview/preview-test.stories.tsx | 15 ++- src/components/preview/preview.component.tsx | 51 +++++++--- src/components/preview/preview.mdx | 31 +++++- src/components/preview/preview.pw.tsx | 13 ++- src/components/preview/preview.stories.tsx | 18 +++- src/components/preview/preview.style.ts | 95 ++++++++++++++++++- src/components/preview/preview.test.tsx | 68 ++++++++++++- 10 files changed, 258 insertions(+), 101 deletions(-) delete mode 100644 src/components/preview/__internal__/preview-placeholder.component.tsx delete mode 100644 src/components/preview/__internal__/preview-placeholder.style.ts diff --git a/src/components/link-preview/link-preview.style.ts b/src/components/link-preview/link-preview.style.ts index 0c5640861e..2e323a41c9 100644 --- a/src/components/link-preview/link-preview.style.ts +++ b/src/components/link-preview/link-preview.style.ts @@ -1,6 +1,8 @@ import styled, { css } from "styled-components"; -import { StyledPreview } from "../preview/preview.style"; -import { StyledPreviewPlaceholder } from "../preview/__internal__/preview-placeholder.style"; +import { + StyledPreview, + StyledPreviewPlaceholder, +} from "../preview/preview.style"; import addFocusStyling from "../../style/utils/add-focus-styling"; import baseTheme from "../../style/themes/base"; diff --git a/src/components/preview/__internal__/preview-placeholder.component.tsx b/src/components/preview/__internal__/preview-placeholder.component.tsx deleted file mode 100644 index 2f20f2f23e..0000000000 --- a/src/components/preview/__internal__/preview-placeholder.component.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; -import tagComponent from "../../../__internal__/utils/helpers/tags/tags"; -import { - StyledPreviewPlaceholder, - StyledPreviewPlaceholderProps, -} from "./preview-placeholder.style"; - -export interface PreviewPlaceholderProps extends StyledPreviewPlaceholderProps { - index: number; - /** The number of lines to render. */ - lines: number; - /* Provides more control over when in a loading state. */ - loading?: boolean; -} - -const PreviewPlaceholder = ({ - height, - index, - lines, - width, - ...props -}: PreviewPlaceholderProps) => { - const isLastLine = lines > 1 && lines === index; - - return ( - - ); -}; - -export default PreviewPlaceholder; diff --git a/src/components/preview/__internal__/preview-placeholder.style.ts b/src/components/preview/__internal__/preview-placeholder.style.ts deleted file mode 100644 index bee0e1b2d3..0000000000 --- a/src/components/preview/__internal__/preview-placeholder.style.ts +++ /dev/null @@ -1,28 +0,0 @@ -import styled, { keyframes } from "styled-components"; - -export interface StyledPreviewPlaceholderProps { - /** A custom height to be applied to the component. */ - height?: string; - /** A custom width */ - width?: string; -} - -const shimmer = keyframes` - 0% { opacity:0.1 } - 70% { opacity:0.6 } - 100% { opacity:0.1 } -`; - -export const StyledPreviewPlaceholder = styled.span` - animation: ${shimmer} 2s ease infinite; - background: var(--colorsUtilityMajor150); - display: block; - height: ${({ height }) => height || "15px"}; - opacity: 0.6; - width: ${({ width }) => width || "100%"}; - border-radius: var(--borderRadius050); - - & + & { - margin-top: 3px; - } -`; diff --git a/src/components/preview/preview-test.stories.tsx b/src/components/preview/preview-test.stories.tsx index ada86651a3..34d89c9b8d 100644 --- a/src/components/preview/preview-test.stories.tsx +++ b/src/components/preview/preview-test.stories.tsx @@ -1,8 +1,9 @@ -import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; import Preview from "./preview.component"; -export default { +const meta: Meta = { title: "Preview/Test", + component: Preview, parameters: { info: { disable: true }, chromatic: { @@ -11,17 +12,13 @@ export default { }, }; -export const Default = ({ children, ...args }: { children?: string }) => ( - {children} -); +export default meta; +type Story = StoryObj; -Default.story = { - name: "default", +export const Default: Story = { args: { children: "Text rendered as children component.", - height: "", lines: 1, loading: true, - width: "", }, }; diff --git a/src/components/preview/preview.component.tsx b/src/components/preview/preview.component.tsx index 78801fb525..cfdeb015cc 100644 --- a/src/components/preview/preview.component.tsx +++ b/src/components/preview/preview.component.tsx @@ -1,40 +1,63 @@ import React from "react"; import { MarginProps } from "styled-system"; - -import PreviewPlaceholder, { - PreviewPlaceholderProps, -} from "./__internal__/preview-placeholder.component"; -import { StyledPreview } from "./preview.style"; +import { StyledPreview, StyledPreviewPlaceholder } from "./preview.style"; import { filterStyledSystemMarginProps } from "../../style/utils"; +import useMediaQuery from "../../hooks/useMediaQuery"; + +export type Shapes = "text" | "rectangle" | "rectangle-round" | "circle"; -export interface PreviewProps - extends Partial>, - MarginProps { +export interface PreviewProps extends MarginProps { /** Children content to render in the component. */ children?: React.ReactNode; - /** Provides more control over when in a loading state. */ + /** Sets loading state. */ loading?: boolean; + /** Sets the height of the Preview. */ + height?: string; + /** Sets the width of the Preview. */ + width?: string; + /** The number of placeholder shapes to render. */ + lines?: number; + /** Sets the preview's shape. */ + shape?: Shapes; + /** Removes Preview's animation, is true when prefer reduce-motion is on. */ + disableAnimation?: boolean; } export const Preview = ({ children, loading, lines = 1, + height, + width, + shape = "text", + disableAnimation, ...props }: PreviewProps) => { const marginProps = filterStyledSystemMarginProps(props); - const hasPlaceholder = loading === undefined ? !children : loading; + const hasPlaceholder = loading ?? !children; + + const isLastLine = (index: number) => { + return lines > 1 && lines === index + 1; + }; + + const reduceMotion = !useMediaQuery( + "screen and (prefers-reduced-motion: no-preference)" + ); if (hasPlaceholder) { const placeholders = []; - for (let i = 1; i <= lines; i++) { + for (let i = 0; i < lines; i++) { placeholders.push( - ); diff --git a/src/components/preview/preview.mdx b/src/components/preview/preview.mdx index 454ec0c5c0..8ded72cb8c 100644 --- a/src/components/preview/preview.mdx +++ b/src/components/preview/preview.mdx @@ -26,22 +26,45 @@ import Preview from "carbon-react/lib/components/preview"; -### Preview with Lines +### With Lines + +You can use the `lines` prop to specify the number of placeholder shapes to render. -### Preview with Children +### With Children + +You can pass children to the component which will render when the `loading` prop is `false` or undefined. -### Preview with Custom Width +### With Custom Width + +You can use the `width` prop to specify the width of the Preview. -### Preview with Custom Height +### With Custom Height + +You can use the `height` prop to specify the height of the Preview. +### Shapes + +By default, the shape of the Preview is "text", however you can use the `shape` prop to change this. +You may also use the `lines` prop to render multiple Previews with the specified shape and the `width` and `height` props to change the default dimensions. + +Note that when the `shape` prop is set to "circle", the `height` prop will determine the diameter and the `width` prop will be ignored. + + + +### Disable Animation + +You can set the `disableAnimation` prop to true to disable the loading animation. This will automatically be set to true when prefer reduce-motion is enabled. + + + ## Props ### Preview diff --git a/src/components/preview/preview.pw.tsx b/src/components/preview/preview.pw.tsx index 0605b4723a..67939cdf14 100644 --- a/src/components/preview/preview.pw.tsx +++ b/src/components/preview/preview.pw.tsx @@ -86,6 +86,17 @@ test.describe("check Preview component properties", () => { expect(elementsCount).toBe(line); }); }); + + test("should render with no animation when the user prefers reduced motion", async ({ + mount, + page, + }) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + + await mount(); + + await expect(lineComponent(page)).toHaveCSS("animation-name", "none"); + }); }); test.describe("Border radius", () => { @@ -95,7 +106,7 @@ test.describe("Border radius", () => { }) => { await mount(); - await expect(lineComponent(page)).toHaveCSS("border-radius", "4px"); + await expect(lineComponent(page)).toHaveCSS("border-radius", "8px"); }); test("should have the expected styling when roundedCornersOptOut is true", async ({ diff --git a/src/components/preview/preview.stories.tsx b/src/components/preview/preview.stories.tsx index 09f8ab7b74..253ef8438c 100644 --- a/src/components/preview/preview.stories.tsx +++ b/src/components/preview/preview.stories.tsx @@ -4,7 +4,7 @@ import { Meta, StoryObj } from "@storybook/react"; import generateStyledSystemProps from "../../../.storybook/utils/styled-system-props"; import Button from "../button"; -import Preview from "./preview.component"; +import Preview from "."; const styledSystemProps = generateStyledSystemProps({ margin: true, @@ -58,3 +58,19 @@ export const WithHeight: Story = () => { return ; }; WithHeight.storyName = "With Height"; + +export const Shapes: Story = () => { + return ( + <> + + + + + ); +}; +Shapes.storyName = "Shapes"; + +export const DisableAnimation: Story = () => { + return ; +}; +DisableAnimation.storyName = "Disable Animation"; diff --git a/src/components/preview/preview.style.ts b/src/components/preview/preview.style.ts index 050d95b418..76f040b747 100644 --- a/src/components/preview/preview.style.ts +++ b/src/components/preview/preview.style.ts @@ -1,14 +1,103 @@ -import styled from "styled-components"; +import styled, { css, keyframes } from "styled-components"; import { margin } from "styled-system"; import baseTheme from "../../style/themes/base"; +import { Shapes } from "./preview.component"; const StyledPreview = styled.div` ${margin} `; +interface StyledPreviewPlaceholderProps { + height?: string; + width?: string; + shape: Shapes; + disableAnimation?: boolean; + isLastLine: boolean; +} + +const shimmer = keyframes` + 0% { + opacity: 0.1 + } + 70% { + opacity: 1 + } + 100% { + opacity: 0.1 + } +`; + +function getBorderRadius(shape: Shapes) { + switch (shape) { + case "rectangle-round": + return "var(--borderRadius400)"; + case "circle": + return "var(--borderRadiusCircle)"; + default: + return "var(--borderRadius100)"; + } +} + +function getHeight(shape: Shapes) { + if (shape.includes("rectangle")) { + return "var(--sizing400)"; + } + + switch (shape) { + case "circle": + return "var(--sizing700)"; + default: + return "var(--sizing175)"; + } +} + +function getWidth(shape: Shapes) { + if (shape.includes("rectangle")) { + return "var(--sizing1500)"; + } + + return "100%"; +} + +const StyledPreviewPlaceholder = styled.span` + ${({ shape, disableAnimation, isLastLine, height, width }) => { + return css` + display: block; + background: linear-gradient( + 135deg, + var(--colorsUtilityMajor100), + var(--colorsUtilityMajor040) + ); + border-radius: ${getBorderRadius(shape)}; + height: ${height || getHeight(shape)}; + width: ${width || getWidth(shape)}; + animation: ${shimmer} 2s ease infinite; + + ${isLastLine && + shape === "text" && + css` + width: calc(${width || getWidth(shape)}*0.8); + `} + + ${shape === "circle" && + css` + width: ${height || getHeight(shape)}; + `} + + ${disableAnimation && + css` + animation: none; + `} + + & + & { + margin-top: 6px; + } + `; + }} +`; + StyledPreview.defaultProps = { theme: baseTheme, }; -// eslint-disable-next-line import/prefer-default-export -export { StyledPreview }; +export { StyledPreview, StyledPreviewPlaceholder }; diff --git a/src/components/preview/preview.test.tsx b/src/components/preview/preview.test.tsx index 31d6616a8a..f6371a0ac1 100644 --- a/src/components/preview/preview.test.tsx +++ b/src/components/preview/preview.test.tsx @@ -41,12 +41,70 @@ test("renders the correct number of placeholders when `lines` prop is set", () = expect(screen.getAllByTestId("preview-placeholder")).toHaveLength(3); }); -test("renders with provided `width` and `height`", () => { - render(); +// coverage +test("renders with the correct height, width and border-radius when `shape` is set to 'text'", () => { + render(); - expect(screen.getByTestId("preview-placeholder")).toHaveStyle({ - width: "100px", - height: "100px", + const placeholder = screen.getByTestId("preview-placeholder"); + + expect(placeholder).toHaveStyleRule("height", "var(--sizing175)"); + expect(placeholder).toHaveStyle({ width: "100%" }); + expect(placeholder).toHaveStyleRule( + "border-radius", + "var(--borderRadius100)" + ); +}); + +// coverage +test("renders with the correct height, width and border-radius when `shape` is set to 'rectangle'", () => { + render(); + + const placeholder = screen.getByTestId("preview-placeholder"); + + expect(placeholder).toHaveStyleRule("height", "var(--sizing400)"); + expect(placeholder).toHaveStyleRule("width", "var(--sizing1500)"); + expect(placeholder).toHaveStyleRule( + "border-radius", + "var(--borderRadius100)" + ); +}); + +// coverage +test("renders with the correct height, width and border-radius when `shape` is set to 'rectangle-round'", () => { + render(); + + const placeholder = screen.getByTestId("preview-placeholder"); + + expect(placeholder).toHaveStyleRule("height", "var(--sizing400)"); + expect(placeholder).toHaveStyleRule("width", "var(--sizing1500)"); + expect(placeholder).toHaveStyleRule( + "border-radius", + "var(--borderRadius400)" + ); +}); + +// coverage +test("renders with the correct height, width and border-radius when `shape` is set to 'circle'", () => { + render(); + + const placeholder = screen.getByTestId("preview-placeholder"); + + expect(placeholder).toHaveStyleRule("height", "var(--sizing700)"); + expect(placeholder).toHaveStyleRule("width", "var(--sizing700)"); + expect(placeholder).toHaveStyleRule( + "border-radius", + "var(--borderRadiusCircle)" + ); +}); + +// coverage +test("renders with no animation when `disableAnimation` is true", () => { + render(); + + const placeholder = screen.getByTestId("preview-placeholder"); + + expect(placeholder).toHaveStyle({ + animation: "none", }); });