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

Merged
merged 15 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 6 additions & 0 deletions .changeset/dull-panthers-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/icons": minor
nkrantz marked this conversation as resolved.
Show resolved Hide resolved
"@twilio-page/core": minor
---

[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": minor
---

[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",
"BlockquoteCitation": "@twilio-paste/core/blockquote",
"BlockquoteContent": "@twilio-paste/core/blockquote",
"Breadcrumb": "@twilio-paste/core/breadcrumb",
"BreadcrumbItem": "@twilio-paste/core/breadcrumb",
"Button": "@twilio-paste/core/button",
Expand Down
Empty file.
88 changes: 88 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,88 @@
import { render, screen } from "@testing-library/react";
import * as React from "react";

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

describe("Blockquote", () => {
it("should render", () => {
render(
<Blockquote url="#" data-testid="blockquote">
<BlockquoteContent>This is some text.</BlockquoteContent>
<BlockquoteCitation 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(text).toHaveAttribute("cite", "#");
Copy link
Collaborator

Choose a reason for hiding this comment

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

👏

expect(blockquote.querySelector(`[data-paste-element='BLOCKQUOTE_CITATION_ANCHOR']`)).toBeTruthy();
});

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

const blockquote = screen.getByTestId("blockquote");
expect(blockquote.querySelector(`[data-paste-element='BLOCKQUOTE_CITATION_CITE']`)).toBeTruthy();
expect(blockquote.querySelector("a")).toBeNull();
expect(screen.getByText("This is some text.")).not.toHaveAttribute("cite");
});

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

const blockquote = screen.getByTestId("blockquote");
expect(blockquote).toBeDefined();
expect(blockquote.querySelector(`[data-paste-element='BLOCKQUOTE_CITATION_CITE']`)).toBeFalsy();
expect(screen.getByText("This is some text.")).not.toHaveAttribute("cite");
});
});

describe("Customization", () => {
it("should set element data attribute", () => {
const { getByTestId } = render(
<Blockquote url="#" data-testid="blockquote">
<BlockquoteContent>This is some text.</BlockquoteContent>
<BlockquoteCitation 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='INNER_BLOCKQUOTE']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_CONTENT']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_CITATION']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_CITATION_AUTHOR']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_CITATION_CITE']`)).toBeTruthy();
expect(getByTestId("blockquote").querySelector(`[data-paste-element='BLOCKQUOTE_CITATION_ANCHOR']`)).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>
<BlockquoteCitation author="Google" source="People + AI Guidebook" element="CUSTOMIZED_SOURCE" />
</Blockquote>,
);
screen.debug();
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='INNER_CUSTOMIZED']`)).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();
});
});
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"
}
}
48 changes: 48 additions & 0 deletions packages/paste-core/components/blockquote/src/Blockquote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Box, safelySpreadBoxProps } from "@twilio-paste/box";
import 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
{...safelySpreadBoxProps(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 element={`INNER_${element}`}>{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.

Did we decide whether this was needed? Does it do weird stuff because the parent is a flex container?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah exactly, it's needed for flex reasons, so I added a customization target and called it a day

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

Blockquote.displayName = "Blockquote";
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Anchor } from "@twilio-paste/anchor";
import { Box, safelySpreadBoxProps } from "@twilio-paste/box";
import 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 BlockquoteCitationProps extends HTMLPasteProps<"div"> {
/**
* Overrides the default element name to apply unique styles with the Customization Provider
* @default 'BLOCKQUOTE_CITATION'
* @type {BoxProps['element']}
* @memberof BlockquoteCitationProps
*/
element?: BoxProps["element"];

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

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

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

return (
<Box
{...safelySpreadBoxProps(props)}
marginTop="space30"
marginBottom="space0"
as="p"
element={element}
ref={ref}
>
—{" "}
<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>
);
},
);

BlockquoteCitation.displayName = "BlockquoteCitation";
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);
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 { BlockquoteCitation } from "./BlockquoteCitation";
export type { BlockquoteCitationProps } from "./BlockquoteCitation";
Loading
Loading