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

feat: create Code Block Package #2641

Merged
merged 4 commits into from
Sep 6, 2022
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
6 changes: 6 additions & 0 deletions .changeset/great-tools-cry.md
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 🎉
6 changes: 6 additions & 0 deletions .changeset/lucky-balloons-joke.md
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.
6 changes: 6 additions & 0 deletions .changeset/tall-falcons-occur.md
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
2 changes: 2 additions & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"/packages/paste-core/components/chat-log",
"/packages/paste-core/components/checkbox",
"/packages/paste-libraries/clipboard-copy",
"/packages/paste-core/components/code-block",
"/packages/paste-color-contrast-utils",
"/packages/paste-core/components/combobox",
"/packages/paste-core/primitives/combobox",
Expand Down Expand Up @@ -66,6 +67,7 @@
"/packages/paste-style-props",
"/packages/paste-libraries/styling",
"/packages/paste-core/components/switch",
"/packages/paste-libraries/syntax-highlighter",
"/packages/paste-core/components/table",
"/packages/paste-core/components/tabs",
"/packages/paste-core/primitives/tabs",
Expand Down
9 changes: 9 additions & 0 deletions @types/react-syntax-highlighter/index.d.ts
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;
Copy link

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?

Copy link
Contributor Author

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?

Copy link

@ghost ghost Sep 6, 2022

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 avoid any types if possible, even preferring something like unknown if we don't have time to determine which type it should be.

export default language;
}

declare module 'react-syntax-highlighter/dist/esm/styles/prism/night-owl' {
const style: { [key: string]: React.CSSProperties };
export default style;
}
1 change: 1 addition & 0 deletions cypress/integration/sitemap-vrt/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const SITEMAP = [
'/components/breadcrumb/',
'/components/badge/',
'/components/callout/',
'/components/code-block/',
'/components/card/',
'/components/chat-log/',
'/components/checkbox/',
Expand Down
6 changes: 6 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link

Choose a reason for hiding this comment

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

for my own edification what do these changes in the .cache/mappings.json file do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 import {whatevs} from '@twilio-paste/whatevs' to import {whatevs} from '@twilio-paste/core/whatevs'

"Combobox": "@twilio-paste/core/combobox",
"ComboboxInputWrapper": "@twilio-paste/core/combobox",
"ComboboxListbox": "@twilio-paste/core/combobox",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,6 @@ describe('Button', () => {
</Button>
)
).toThrow();
expect(() =>
shleewhite marked this conversation as resolved.
Show resolved Hide resolved
render(
<Button as="a" href="#" variant="inverse">
Go to Paste
</Button>
)
).toThrow();
spy.mockRestore();
});

Expand Down
4 changes: 2 additions & 2 deletions packages/paste-core/components/button/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
Copy link

@ghost ghost Sep 1, 2022

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.`);
Expand Down
2 changes: 2 additions & 0 deletions packages/paste-core/components/button/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface DirectButtonProps extends React.ButtonHTMLAttributes<HTMLButton
buttonState: ButtonStates;
variant: ButtonVariants;
pressed?: boolean;
target?: string;
}

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, Pick<BoxProps, 'element'> {
Expand All @@ -61,4 +62,5 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
children: React.ReactNode;
i18nExternalLinkLabel?: string;
pressed?: boolean;
target?: string;
}
Empty file.
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)');
});
});
});
Loading