-
Notifications
You must be signed in to change notification settings - Fork 115
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
feat: create Code Block Package #2641
Changes from all commits
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,6 @@ | ||
--- | ||
'@twilio-paste/code-block': major | ||
'@twilio-paste/core': minor | ||
--- | ||
|
||
[Code Block] create a new CodeBlock component, which allows you to display readable blocks of code 🎉 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@twilio-paste/core': minor | ||
'@twilio-paste/syntax-highlighter-library': major | ||
--- | ||
|
||
[Syntax Highlighter] Create a new library, @twilio-paste/syntax-highlighter-library. Supports: javascript, jsx, csharp, php, ruby, python, java, json, c, bash, shell, go, and groovy. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@twilio-paste/button': patch | ||
'@twilio-paste/core': patch | ||
--- | ||
|
||
[Button] support <Button as='a' variant='inverse'> and add the `target` prop |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
declare module 'react-syntax-highlighter/dist/esm/languages/prism/shell-session' { | ||
const language: any; | ||
export default language; | ||
} | ||
|
||
declare module 'react-syntax-highlighter/dist/esm/styles/prism/night-owl' { | ||
const style: { [key: string]: React.CSSProperties }; | ||
export default style; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,12 @@ | |
"Checkbox": "@twilio-paste/core/checkbox", | ||
"CheckboxDisclaimer": "@twilio-paste/core/checkbox", | ||
"CheckboxGroup": "@twilio-paste/core/checkbox", | ||
"CodeBlock": "@twilio-paste/core/code-block", | ||
"CodeBlockHeader": "@twilio-paste/core/code-block", | ||
"CodeBlockTab": "@twilio-paste/core/code-block", | ||
"CodeBlockTabList": "@twilio-paste/core/code-block", | ||
"CodeBlockTabPanel": "@twilio-paste/core/code-block", | ||
"CodeBlockWrapper": "@twilio-paste/core/code-block", | ||
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. for my own edification what do these changes in the 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. This is for a codemod that helps people transition from barreled to unbarreled imports. It is used here: https://github.com/twilio-labs/paste/blob/main/packages/paste-codemods/transforms/barreled-to-unbarreled.js to check what people are importing and change it from |
||
"Combobox": "@twilio-paste/core/combobox", | ||
"ComboboxInputWrapper": "@twilio-paste/core/combobox", | ||
"ComboboxListbox": "@twilio-paste/core/combobox", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,8 +88,8 @@ const handlePropValidation = ({ | |
if (variant === 'link' || variant === 'inverse_link') { | ||
throw new Error(`[Paste: Button] Using Button component as an Anchor. Use the Paste Anchor component instead.`); | ||
} | ||
if (variant !== 'primary' && variant !== 'secondary' && variant !== 'reset') { | ||
throw new Error(`[Paste: Button] <Button as="a"> only works with the following variants: primary or secondary.`); | ||
if (variant !== 'primary' && variant !== 'secondary' && variant !== 'reset' && variant !== 'inverse') { | ||
throw new Error(`[Paste: Button] <Button as="a"> only works with the following variants: primary and secondary.`); | ||
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. its interesting that we include usage enforcement code like this as part of the run time! One thing we might explore in the future is handling this kind of thing through dev time only type annotations or react propTypes! cc @TheSisb 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. Totally.. I don't have full context here but I think we usually try to do this stuff with types/propTypes, but for really important things we actually throw errors. We throw errors in Avatar and Badge too. |
||
} | ||
if (disabled || loading) { | ||
throw new Error(`[Paste: Button] <Button as="a"> cannot be disabled or loading.`); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import * as React from 'react'; | ||
import {CustomizationProvider} from '@twilio-paste/customization'; | ||
import {render, screen} from '@testing-library/react'; | ||
|
||
import {CodeBlock, CodeBlockWrapper, CodeBlockHeader, CodeBlockTabList, CodeBlockTab, CodeBlockTabPanel} from '../src'; | ||
|
||
const jsCode = `(num) => num + 1`; | ||
|
||
const CustomizationWrapper: React.FC = ({children}) => ( | ||
<CustomizationProvider | ||
baseTheme="default" | ||
theme={TestTheme} | ||
elements={{ | ||
CODE_BLOCK_CONTENT: {width: 'size50'}, | ||
CODE_BLOCK_COPY_BUTTON: {backgroundColor: 'colorBackgroundErrorWeakest'}, | ||
CODE_BLOCK_EXTERNAL_LINK: {backgroundColor: 'colorBackgroundErrorWeakest'}, | ||
CODE_BLOCK_HEADER: {borderTopRightRadius: 'borderRadius30'}, | ||
CODE_BLOCK_TAB_LIST: {columnGap: 'space0'}, | ||
CODE_BLOCK_TAB_PANEL: {borderBottomRightRadius: 'borderRadius30'}, | ||
CODE_BLOCK_TAB: {borderRadius: 'borderRadius0'}, | ||
CODE_BLOCK_WRAPPER: {width: 'size50'}, | ||
CODE_BLOCK: {width: 'size50'}, | ||
}} | ||
> | ||
{children} | ||
</CustomizationProvider> | ||
); | ||
|
||
const CustomizationMyWrapper: React.FC = ({children}) => ( | ||
<CustomizationProvider | ||
baseTheme="default" | ||
theme={TestTheme} | ||
elements={{ | ||
MY_CODE_BLOCK_CONTENT: {width: 'size50'}, | ||
MY_CODE_BLOCK_COPY_BUTTON: {backgroundColor: 'colorBackgroundErrorWeakest'}, | ||
MY_CODE_BLOCK_EXTERNAL_LINK: {backgroundColor: 'colorBackgroundErrorWeakest'}, | ||
MY_CODE_BLOCK_HEADER: {borderTopRightRadius: 'borderRadius30'}, | ||
MY_CODE_BLOCK_TAB_LIST: {columnGap: 'space0'}, | ||
MY_CODE_BLOCK_TAB_PANEL: {borderBottomRightRadius: 'borderRadius30'}, | ||
MY_CODE_BLOCK_TAB: {borderRadius: 'borderRadius0'}, | ||
MY_CODE_BLOCK_WRAPPER: {width: 'size50'}, | ||
MY_CODE_BLOCK: {width: 'size50'}, | ||
}} | ||
> | ||
{children} | ||
</CustomizationProvider> | ||
); | ||
|
||
describe('Customization', () => { | ||
describe('CodeBlock', () => { | ||
it('should set a default element data attribute', () => { | ||
render( | ||
<CodeBlockWrapper> | ||
<CodeBlockHeader>My code block</CodeBlockHeader> | ||
<CodeBlockTabList> | ||
<CodeBlockTab>JavaScript</CodeBlockTab> | ||
</CodeBlockTabList> | ||
<CodeBlockTabPanel> | ||
<CodeBlock language="javascript" code={jsCode} data-testid="code-block" externalLink="www.google.com" /> | ||
</CodeBlockTabPanel> | ||
</CodeBlockWrapper>, | ||
{ | ||
wrapper: CustomizationWrapper, | ||
} | ||
); | ||
|
||
const codeBlock = screen.getByTestId('code-block'); | ||
const content = codeBlock.querySelector('pre')?.parentElement; | ||
const heading = screen.getByRole('heading', {name: 'My code block'}); | ||
const wrapper = heading.parentElement; | ||
const tabList = screen.getByRole('tablist'); | ||
const tab = screen.getByRole('tab', {name: 'JavaScript'}); | ||
const tabPanel = codeBlock.parentElement; | ||
const copyButton = screen.getByRole('button', {name: 'Copy code block'}); | ||
const externalLink = screen.getByRole('link', {name: 'Open code block in new page'}); | ||
|
||
expect(wrapper?.getAttribute('data-paste-element')).toBe('CODE_BLOCK_WRAPPER'); | ||
expect(content?.getAttribute('data-paste-element')).toBe('CODE_BLOCK_CONTENT'); | ||
expect(tabList.getAttribute('data-paste-element')).toBe('CODE_BLOCK_TAB_LIST'); | ||
expect(tab.getAttribute('data-paste-element')).toBe('CODE_BLOCK_TAB'); | ||
expect(tabPanel?.getAttribute('data-paste-element')).toBe('CODE_BLOCK_TAB_PANEL'); | ||
expect(codeBlock.getAttribute('data-paste-element')).toBe('CODE_BLOCK'); | ||
expect(heading.getAttribute('data-paste-element')).toBe('CODE_BLOCK_HEADER'); | ||
expect(copyButton.getAttribute('data-paste-element')).toBe('CODE_BLOCK_COPY_BUTTON'); | ||
expect(externalLink.getAttribute('data-paste-element')).toBe('CODE_BLOCK_EXTERNAL_LINK'); | ||
}); | ||
|
||
it('should set a custom element data attribute', () => { | ||
render( | ||
<CodeBlockWrapper data-testid="wrapper" element="MY_CODE_BLOCK_WRAPPER"> | ||
<CodeBlockHeader element="MY_CODE_BLOCK_HEADER">My code block</CodeBlockHeader> | ||
<CodeBlockTabList element="MY_CODE_BLOCK_TAB_LIST"> | ||
<CodeBlockTab element="MY_CODE_BLOCK_TAB">JavaScript</CodeBlockTab> | ||
</CodeBlockTabList> | ||
<CodeBlockTabPanel element="MY_CODE_BLOCK_TAB_PANEL"> | ||
<CodeBlock | ||
language="javascript" | ||
code={jsCode} | ||
data-testid="code-block" | ||
externalLink="www.google.com" | ||
element="MY_CODE_BLOCK" | ||
/> | ||
</CodeBlockTabPanel> | ||
</CodeBlockWrapper>, | ||
{ | ||
wrapper: CustomizationMyWrapper, | ||
} | ||
); | ||
|
||
const codeBlock = screen.getByTestId('code-block'); | ||
const content = codeBlock.querySelector('pre')?.parentElement; | ||
const heading = screen.getByRole('heading', {name: 'My code block'}); | ||
const wrapper = heading.parentElement; | ||
const tabList = screen.getByRole('tablist'); | ||
const tab = screen.getByRole('tab', {name: 'JavaScript'}); | ||
const tabPanel = codeBlock.parentElement; | ||
const copyButton = screen.getByRole('button', {name: 'Copy code block'}); | ||
const externalLink = screen.getByRole('link', {name: 'Open code block in new page'}); | ||
|
||
expect(wrapper?.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_WRAPPER'); | ||
expect(content?.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_CONTENT'); | ||
expect(tabList.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_TAB_LIST'); | ||
expect(tab.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_TAB'); | ||
expect(tabPanel?.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_TAB_PANEL'); | ||
expect(codeBlock.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK'); | ||
expect(heading.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_HEADER'); | ||
expect(copyButton.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_COPY_BUTTON'); | ||
expect(externalLink.getAttribute('data-paste-element')).toBe('MY_CODE_BLOCK_EXTERNAL_LINK'); | ||
}); | ||
|
||
it('should add custom styles to the component', () => { | ||
render( | ||
<CodeBlockWrapper> | ||
<CodeBlockHeader>My code block</CodeBlockHeader> | ||
<CodeBlockTabList> | ||
<CodeBlockTab>JavaScript</CodeBlockTab> | ||
</CodeBlockTabList> | ||
<CodeBlockTabPanel> | ||
<CodeBlock language="javascript" code={jsCode} data-testid="code-block" externalLink="www.google.com" /> | ||
</CodeBlockTabPanel> | ||
</CodeBlockWrapper>, | ||
{ | ||
wrapper: CustomizationWrapper, | ||
} | ||
); | ||
|
||
const codeBlock = screen.getByTestId('code-block'); | ||
const content = codeBlock.querySelector('pre')?.parentElement; | ||
const heading = screen.getByRole('heading', {name: 'My code block'}); | ||
const wrapper = heading.parentElement; | ||
const tabList = screen.getByRole('tablist'); | ||
const tab = screen.getByRole('tab', {name: 'JavaScript'}); | ||
const tabPanel = codeBlock.parentElement; | ||
const copyButton = screen.getByRole('button', {name: 'Copy code block'}); | ||
const externalLink = screen.getByRole('link', {name: 'Open code block in new page'}); | ||
|
||
expect(codeBlock).toHaveStyleRule('width', '31.5rem'); | ||
expect(content).toHaveStyleRule('width', '31.5rem'); | ||
expect(wrapper).toHaveStyleRule('width', '31.5rem'); | ||
expect(heading).toHaveStyleRule('border-top-right-radius', '8px'); | ||
expect(tabList).toHaveStyleRule('column-gap', '0'); | ||
expect(tab).toHaveStyleRule('border-radius', '0'); | ||
expect(tabPanel).toHaveStyleRule('border-bottom-right-radius', '8px'); | ||
expect(copyButton).toHaveStyleRule('background-color', 'rgb(254, 236, 236)'); | ||
expect(externalLink).toHaveStyleRule('background-color', 'rgb(254, 236, 236)'); | ||
}); | ||
|
||
it('should set custom styles with custom element names', () => { | ||
render( | ||
<CodeBlockWrapper data-testid="wrapper" element="MY_CODE_BLOCK_WRAPPER"> | ||
<CodeBlockHeader element="MY_CODE_BLOCK_HEADER">My code block</CodeBlockHeader> | ||
<CodeBlockTabList element="MY_CODE_BLOCK_TAB_LIST"> | ||
<CodeBlockTab element="MY_CODE_BLOCK_TAB">JavaScript</CodeBlockTab> | ||
</CodeBlockTabList> | ||
<CodeBlockTabPanel element="MY_CODE_BLOCK_TAB_PANEL"> | ||
<CodeBlock | ||
language="javascript" | ||
code={jsCode} | ||
data-testid="code-block" | ||
externalLink="www.google.com" | ||
element="MY_CODE_BLOCK" | ||
/> | ||
</CodeBlockTabPanel> | ||
</CodeBlockWrapper>, | ||
{ | ||
wrapper: CustomizationMyWrapper, | ||
} | ||
); | ||
|
||
const codeBlock = screen.getByTestId('code-block'); | ||
const content = codeBlock.querySelector('pre')?.parentElement; | ||
const heading = screen.getByRole('heading', {name: 'My code block'}); | ||
const wrapper = heading.parentElement; | ||
const tabList = screen.getByRole('tablist'); | ||
const tab = screen.getByRole('tab', {name: 'JavaScript'}); | ||
const tabPanel = codeBlock.parentElement; | ||
const copyButton = screen.getByRole('button', {name: 'Copy code block'}); | ||
const externalLink = screen.getByRole('link', {name: 'Open code block in new page'}); | ||
|
||
expect(codeBlock).toHaveStyleRule('width', '31.5rem'); | ||
expect(content).toHaveStyleRule('width', '31.5rem'); | ||
expect(wrapper).toHaveStyleRule('width', '31.5rem'); | ||
expect(heading).toHaveStyleRule('border-top-right-radius', '8px'); | ||
expect(tabList).toHaveStyleRule('column-gap', '0'); | ||
expect(tab).toHaveStyleRule('border-radius', '0'); | ||
expect(tabPanel).toHaveStyleRule('border-bottom-right-radius', '8px'); | ||
expect(copyButton).toHaveStyleRule('background-color', 'rgb(254, 236, 236)'); | ||
expect(externalLink).toHaveStyleRule('background-color', 'rgb(254, 236, 236)'); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor, but this isn't a
string
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TheSisb @gloriliale do either of you have context for why this file is needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shleewhite typically I write an ambient module declaration like this when a 3rd party package doesn't ship with its own type annotations or a type repository like DefinitelyTyped doesn't have community types available. That all said my guess is that this package
react-syntax-highlighter
was missing some types so we added them. Going a bit further to my original question, generally we'd want to avoidany
types if possible, even preferring something likeunknown
if we don't have time to determine which type it should be.