Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create blockquote #4168

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/dull-horses-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@twilio-paste/codemods": minor
---

[Blockquote]: Add new component
5 changes: 5 additions & 0 deletions .changeset/dull-panthers-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@twilio-paste/icons": minor
Copy link
Collaborator

Choose a reason for hiding this comment

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

issue(blocking): add core

---

[Icon]: Add Blockquote icon.
6 changes: 6 additions & 0 deletions .changeset/quiet-windows-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/box": minor
"@twilio-paste/core": minor
---

[Box]: Add cite prop for use with Blockquote
6 changes: 6 additions & 0 deletions .changeset/tough-dolphins-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/blockquote": major
"@twilio-paste/core": major
Copy link
Collaborator

Choose a reason for hiding this comment

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

issue(blocking): minor for core

---

[Blockquote]: Added a new Blockquote component to library to act as a stylized text wrapper for a quotation and source.
1 change: 1 addition & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"/packages/paste-core/components/avatar",
"/packages/paste-core/components/badge",
"/packages/paste-core/components/base-radio-checkbox",
"/packages/paste-core/components/blockquote",
"/packages/paste-core/primitives/box",
"/packages/paste-core/components/breadcrumb",
"/packages/paste-core/components/button",
Expand Down
3 changes: 3 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"BaseRadioCheckboxHelpText": "@twilio-paste/core/base-radio-checkbox",
"BaseRadioCheckboxLabel": "@twilio-paste/core/base-radio-checkbox",
"BaseRadioCheckboxLabelText": "@twilio-paste/core/base-radio-checkbox",
"Blockquote": "@twilio-paste/core/blockquote",
"BlockquoteContent": "@twilio-paste/core/blockquote",
"BlockquoteSource": "@twilio-paste/core/blockquote",
"Breadcrumb": "@twilio-paste/core/breadcrumb",
"BreadcrumbItem": "@twilio-paste/core/breadcrumb",
"Button": "@twilio-paste/core/button",
Expand Down
1 change: 1 addition & 0 deletions packages/paste-core/components/blockquote/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`
Copy link
Collaborator

Choose a reason for hiding this comment

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

such a nitpick but did this happen automatically from the plop template?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

Should be removed as t would show up on the docs page for the component

102 changes: 102 additions & 0 deletions packages/paste-core/components/blockquote/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { render, screen } from "@testing-library/react";
import * as React from "react";

import { Blockquote, BlockquoteContent, BlockquoteSource } from "../src";

describe("Blockquote", () => {
it("should render", () => {
render(
<Blockquote url="#" data-testid="blockquote">
<BlockquoteContent>This is some text.</BlockquoteContent>
<BlockquoteSource author="Google" source="People + AI Guidebook" />
</Blockquote>,
);

const blockquote = screen.getByTestId("blockquote");
expect(blockquote).toBeDefined();

const text = screen.getByText("This is some text.");
expect(text.nodeName).toBe("BLOCKQUOTE");
expect(blockquote.querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE_ANCHOR']`)).toBeTruthy();
});

it("should render without a url", () => {
render(
<Blockquote data-testid="blockquote">
<BlockquoteContent>This is some text.</BlockquoteContent>
<BlockquoteSource author="Google" source="People + AI Guidebook" />
</Blockquote>,
);

const blockquote = screen.getByTestId("blockquote");
expect(blockquote).toBeDefined();
expect(blockquote.querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE_TEXT']`)).toBeTruthy();
Copy link
Collaborator

Choose a reason for hiding this comment

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

There is no test to be sure BLOCKQUOTE_SOURCE_ANCHOR does not exist. Should also be testing for BLOCKQUOTE_SOURCE_CITE in these too

});

it("should render without a source", () => {
render(
<Blockquote url="#" data-testid="blockquote">
<BlockquoteContent>This is some text.</BlockquoteContent>
<BlockquoteSource author="Google" />
</Blockquote>,
);

const blockquote = screen.getByTestId("blockquote");
expect(blockquote).toBeDefined();
expect(blockquote.querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE_CITE']`)).toBeFalsy();
Copy link
Collaborator

Choose a reason for hiding this comment

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

❤️ like this

});
});

describe("Customization", () => {
it("should set element data attribute", () => {
const { getByTestId } = render(
<Blockquote url="#" data-testid="blockquote">
<BlockquoteContent>This is some text.</BlockquoteContent>
<BlockquoteSource author="Google" source="People + AI Guidebook" />
</Blockquote>,
);
const { getByTestId: idWithoutURL } = render(
<Blockquote data-testid="blockquoteWithoutURL">
<BlockquoteContent>This is some text.</BlockquoteContent>
<BlockquoteSource author="Google" source="People + AI Guidebook" />
</Blockquote>,
);

expect(getByTestId("blockquote").getAttribute("data-paste-element")).toEqual("BLOCKQUOTE");
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_ICON']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_CONTENT']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE_AUTHOR']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE_CITE']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE_ANCHOR']`)).toBeTruthy();
expect(
idWithoutURL("blockquoteWithoutURL").querySelector(`[data-paste-element='BLOCKQUOTE_SOURCE_TEXT']`),
).toBeTruthy();
});

it("should set custom element data attribute", () => {
const { getByTestId } = render(
<Blockquote url="#" data-testid="blockquote" element="CUSTOMIZED">
<BlockquoteContent element="CUSTOMIZED_CONTENT">This is some text.</BlockquoteContent>
<BlockquoteSource author="Google" source="People + AI Guidebook" element="CUSTOMIZED_SOURCE" />
</Blockquote>,
);
const { getByTestId: idWithoutURL } = render(
<Blockquote data-testid="blockquoteWithoutURL" element="CUSTOMIZED">
<BlockquoteContent element="CUSTOMIZED_CONTENT">This is some text.</BlockquoteContent>
<BlockquoteSource author="Google" source="People + AI Guidebook" element="CUSTOMIZED_SOURCE" />
</Blockquote>,
);

expect(getByTestId("blockquote").getAttribute("data-paste-element")).toEqual("CUSTOMIZED");
expect(getByTestId("blockquote").querySelector(`[data-paste-element='CUSTOMIZED_ICON']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='CUSTOMIZED_CONTENT']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='CUSTOMIZED_SOURCE']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='CUSTOMIZED_SOURCE_AUTHOR']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='CUSTOMIZED_SOURCE_CITE']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='CUSTOMIZED_SOURCE_ANCHOR']`)).toBeTruthy();
expect(
idWithoutURL("blockquoteWithoutURL").querySelector(`[data-paste-element='CUSTOMIZED_SOURCE_TEXT']`),
).toBeTruthy();
});
Comment on lines +51 to +101
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: these tests can be combined

});
3 changes: 3 additions & 0 deletions packages/paste-core/components/blockquote/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { build } = require("../../../../tools/build/esbuild");

build(require("./package.json"));
75 changes: 75 additions & 0 deletions packages/paste-core/components/blockquote/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@twilio-paste/blockquote",
"version": "0.0.0",
"category": "layout",
"status": "production",
"description": "A Blockquote is a stylized text wrapper for a quotation and source.",
"author": "Twilio Inc.",
"license": "MIT",
"main:dev": "src/index.tsx",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "yarn clean && NODE_ENV=production node build.js && tsc",
"build:js": "NODE_ENV=development node build.js",
"build:typedocs": "tsx ../../../../tools/build/generate-type-docs",
"clean": "rm -rf ./dist",
"tsc": "tsc"
},
"peerDependencies": {
"@twilio-paste/anchor": "^12.0.0",
"@twilio-paste/animation-library": "^2.0.0",
"@twilio-paste/box": "^10.0.0",
"@twilio-paste/button": "^14.0.0",
"@twilio-paste/color-contrast-utils": "^5.0.0",
"@twilio-paste/customization": "^8.0.0",
"@twilio-paste/design-tokens": "^10.0.0",
"@twilio-paste/icons": "^12.0.0",
"@twilio-paste/screen-reader-only": "^13.0.0",
"@twilio-paste/spinner": "^14.0.0",
"@twilio-paste/stack": "^8.0.0",
"@twilio-paste/style-props": "^9.0.0",
"@twilio-paste/styling-library": "^3.0.0",
"@twilio-paste/text": "^10.0.0",
"@twilio-paste/theme": "^11.0.0",
"@twilio-paste/types": "^6.0.0",
"@twilio-paste/uid-library": "^2.0.0",
"@types/react": "^16.8.6 || ^17.0.2 || ^18.0.27",
"@types/react-dom": "^16.8.6 || ^17.0.2 || ^18.0.10",
"react": "^16.8.6 || ^17.0.2 || ^18.0.0",
"react-dom": "^16.8.6 || ^17.0.2 || ^18.0.0"
},
"devDependencies": {
"@twilio-paste/anchor": "^12.0.0",
"@twilio-paste/animation-library": "^2.0.0",
"@twilio-paste/box": "^10.1.0",
"@twilio-paste/button": "^14.1.0",
"@twilio-paste/color-contrast-utils": "^5.0.0",
"@twilio-paste/customization": "^8.1.0",
"@twilio-paste/design-tokens": "^10.9.0",
"@twilio-paste/icons": "^12.2.0",
"@twilio-paste/screen-reader-only": "^13.1.0",
"@twilio-paste/spinner": "^14.0.0",
"@twilio-paste/stack": "^8.0.0",
"@twilio-paste/style-props": "^9.1.0",
"@twilio-paste/styling-library": "^3.0.0",
"@twilio-paste/text": "^10.1.0",
"@twilio-paste/theme": "^11.0.0",
"@twilio-paste/types": "^6.0.0",
"@twilio-paste/uid-library": "^2.0.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tsx": "^4.0.0",
"typescript": "^4.9.4"
}
}
47 changes: 47 additions & 0 deletions packages/paste-core/components/blockquote/src/Blockquote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Box, type BoxProps } from "@twilio-paste/box";
import { BlockquoteIcon } from "@twilio-paste/icons/esm/BlockquoteIcon";
import type { HTMLPasteProps } from "@twilio-paste/types";
import React from "react";

import { BlockquoteContext } from "./BlockquoteContext";

export interface BlockquoteProps extends HTMLPasteProps<"div"> {
children?: React.ReactNode;
/**
* Overrides the default element name to apply unique styles with the Customization Provider
* @default 'BLOCKQUOTE'
* @type {BoxProps['element']}
* @memberof BlockquoteProps
*/
element?: BoxProps["element"];
/**
* The URL to the source of the quote
* @type {string}
* @memberof BlockquoteProps
*/
url?: string;
}

export const Blockquote = React.forwardRef<HTMLDivElement, BlockquoteProps>(
({ children, element = "BLOCKQUOTE", url, ...props }, ref) => {
return (
<Box
{...props}
ref={ref}
display="flex"
columnGap="space50"
alignItems="flex-start"
lineHeight="lineHeight30"
fontSize="fontSize30"
element={element}
>
<BlockquoteIcon element={`${element}_ICON`} decorative={true} color="colorTextIcon" />
<BlockquoteContext.Provider value={{ url }}>
<Box>{children}</Box>
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: is this box wrapping children necessary?

</BlockquoteContext.Provider>
</Box>
);
},
);

Blockquote.displayName = "Blockquote";
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Box, type BoxProps, safelySpreadBoxProps } from "@twilio-paste/box";
import type { HTMLPasteProps } from "@twilio-paste/types";
import React from "react";

import { BlockquoteContext } from "./BlockquoteContext";

export interface BlockquoteContentProps extends HTMLPasteProps<"blockquote"> {
children?: React.ReactNode;
/**
* Overrides the default element name to apply unique styles with the Customization Provider
* @default 'BLOCKQUOTE_CONTENT'
* @type {BoxProps['element']}
* @memberof BlockquoteContentProps
*/
element?: BoxProps["element"];
}

export const BlockquoteContent = React.forwardRef<HTMLQuoteElement, BlockquoteContentProps>(
({ children, element = "BLOCKQUOTE_CONTENT", ...props }, ref) => {
const { url } = React.useContext(BlockquoteContext);

return (
<Box {...safelySpreadBoxProps(props)} as="blockquote" margin="space0" ref={ref} element={element} cite={url}>
{children}
</Box>
);
},
);

BlockquoteContent.displayName = "BlockquoteContent";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as React from "react";

export interface BlockquoteContextProps {
url?: string;
}
export const BlockquoteContext = React.createContext<BlockquoteContextProps>({} as any);
64 changes: 64 additions & 0 deletions packages/paste-core/components/blockquote/src/BlockquoteSource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Anchor } from "@twilio-paste/anchor";
import { Box, type BoxProps } from "@twilio-paste/box";
import { Text } from "@twilio-paste/text";
import type { HTMLPasteProps } from "@twilio-paste/types";
import React from "react";

import { BlockquoteContext } from "./BlockquoteContext";

export interface BlockquoteSourceProps extends HTMLPasteProps<"div"> {
/**
* Overrides the default element name to apply unique styles with the Customization Provider
* @default 'BLOCKQUOTE_SOURCE'
* @type {BoxProps['element']}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @type {BoxProps['element']}
* @type string

right? or am I doing it wrong.

* @memberof BlockquoteSourceProps
*/
element?: BoxProps["element"];

/**
* The author of the quote
* @type {string}
* @memberof BlockquoteSourceProps
*/
author: string;

/**
* The source of the quote
* @type {string}
* @memberof BlockquoteSourceProps
*/
source?: string;
}

export const BlockquoteSource = React.forwardRef<HTMLDivElement, BlockquoteSourceProps>(
({ element = "BLOCKQUOTE_SOURCE", author, source, ...props }, ref) => {
const { url } = React.useContext(BlockquoteContext);

return (
<Box marginTop="space30" as="p" element={element} {...props} ref={ref}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: why the mix of box as p and text as span here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think span is right in this case otherwise they wuld be on separate line. The author and source styling are different

—{" "}
<Text as="span" fontWeight="fontWeightSemibold" element={`${element}_AUTHOR`}>
{author}
</Text>
{source ? (
<>
,{" "}
<Box as="cite" fontStyle="normal" element={`${element}_CITE`}>
{url ? (
<Anchor href={url} showExternal element={`${element}_ANCHOR`}>
{source}
</Anchor>
) : (
<Text as="span" element={`${element}_TEXT`}>
{source}
</Text>
)}
</Box>
</>
) : null}
</Box>
);
},
);

BlockquoteSource.displayName = "BlockquoteSource";
6 changes: 6 additions & 0 deletions packages/paste-core/components/blockquote/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { Blockquote } from "./Blockquote";
export type { BlockquoteProps } from "./Blockquote";
export { BlockquoteContent } from "./BlockquoteContent";
export type { BlockquoteContentProps } from "./BlockquoteContent";
export { BlockquoteSource } from "./BlockquoteSource";
export type { BlockquoteSourceProps } from "./BlockquoteSource";
Loading
Loading