-
Notifications
You must be signed in to change notification settings - Fork 119
Create blockquote #4168
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
Changes from 13 commits
423caa2
bef0fe6
d9a03b8
e55f723
e45bf3f
ef35639
9947536
1b05f70
140213f
c720773
5ce6a58
87fc268
1d3f787
1c51f33
fd6a26b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@twilio-paste/codemods": minor | ||
--- | ||
|
||
[Blockquote]: Add new component |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@twilio-paste/icons": minor | ||
"@twilio-page/core": minor | ||
--- | ||
|
||
[Icon]: Add Blockquote icon. |
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 |
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. |
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", "#"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const { build } = require("../../../../tools/build/esbuild"); | ||
|
||
build(require("./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" | ||
} | ||
} |
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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,65 @@ | ||
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" 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); |
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"; |
Uh oh!
There was an error while loading. Please reload this page.