Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 6.5k
(website redesign) Feat(shellbox): migration#5234
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
1ec7c301df6c685aca0750e8441b3bf9eb60d9d5b6ef6794f4b5e672efba032ba2ea120f12d241c210d678df7e7aae068cf6d55381dcbd5af101035c190801d52080b3a70e09f09de81deaacc8eb7d59686b9908b4d9e23d5f4fc4c3d66e2cfd17ddc04c221562277266854569166780d6d72fe2c48a87ac3a56e6b36262455c087e2f0fc3fb857df26c9909d85404ca91898bd1286f1d329e2d7b70abc5b40d523040980cc67076dbf1eeba3dc746d23c323b50466f9232761d0f5721f2d7f06File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||
| exports[`ShellBox should render 1`] = ` | ||
| <div> | ||
| <pre | ||
| class="shellBox" | ||
| > | ||
| <div | ||
| class="top" | ||
| > | ||
| <span> | ||
| SHELL | ||
| </span> | ||
| <button | ||
| type="button" | ||
| > | ||
| components.common.shellBox.copy | ||
| </button> | ||
| </div> | ||
| <code> | ||
| test | ||
| </code> | ||
| </pre> | ||
| </div> | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { render, screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { IntlProvider } from 'react-intl'; | ||
| import ShellBox from '../index'; | ||
| const mockWriteText = jest.fn(); | ||
| const originalNavigator = { ...window.navigator }; | ||
| describe('ShellBox', () => { | ||
| beforeEach(() => { | ||
| Object.defineProperty(window, 'navigator', { | ||
| value: { | ||
| clipboard: { | ||
| writeText: mockWriteText, | ||
| }, | ||
| }, | ||
| }); | ||
| }); | ||
| afterEach(() => { | ||
| Object.defineProperty(window, 'navigator', { | ||
| value: originalNavigator, | ||
| }); | ||
| }); | ||
| it('should render', () => { | ||
| const { container } = render( | ||
| <IntlProvider locale="en" onError={() => {}}> | ||
| <ShellBox>test</ShellBox> | ||
| </IntlProvider> | ||
| ); | ||
| expect(container).toMatchSnapshot(); | ||
| }); | ||
| it('should call clipboard API with `test` once', async () => { | ||
| const user = userEvent.setup(); | ||
| const navigatorClipboardWriteTextSpy = jest | ||
| .fn() | ||
| .mockImplementation(() => Promise.resolve()); | ||
| Object.defineProperty(window.navigator, 'clipboard', { | ||
| writable: true, | ||
| value: { | ||
| writeText: navigatorClipboardWriteTextSpy, | ||
| }, | ||
| }); | ||
| render( | ||
| <IntlProvider locale="en" onError={() => {}}> | ||
| <ShellBox textToCopy="test">test</ShellBox> | ||
| </IntlProvider> | ||
| ); | ||
| const button = screen.getByRole('button'); | ||
| await user.click(button); | ||
| expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1); | ||
| expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith('test'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| .shellBox { | ||
| background-color: var(--black10); | ||
| border-radius: 0.4rem; | ||
| box-sizing: border-box; | ||
| display: flex; | ||
| flex-direction: column; | ||
| font-family: var(--mono); | ||
| padding: 0 0 var(--space-48) var(--space-16); | ||
| position: relative; | ||
| code { | ||
| color: var(--pink5); | ||
ovflowd marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| font-family: inherit; | ||
| line-height: 30px; | ||
| overflow-x: hidden; | ||
| position: absolute; | ||
| top: 30px; | ||
| width: calc(100% - 20px); | ||
| &:hover { | ||
| overflow-x: auto; | ||
| } | ||
| &::-webkit-scrollbar { | ||
| height: 0.5em; | ||
| } | ||
| &::-webkit-scrollbar, | ||
| &::-webkit-scrollbar-thumb { | ||
| border-radius: 4px; | ||
| overflow: visible; | ||
| } | ||
| &::-webkit-scrollbar-thumb { | ||
| background: rgba(0, 0, 0, 0.2); | ||
| } | ||
| > span.function { | ||
| color: var(--warning5); | ||
| } | ||
| } | ||
| .top { | ||
| display: inherit; | ||
| flex-direction: row; | ||
| justify-content: space-between; | ||
| margin-bottom: var(--space-08); | ||
| span, | ||
| button { | ||
| align-items: center; | ||
| display: inherit; | ||
| font-size: var(--font-size-code); | ||
| height: 23px; | ||
| justify-content: center; | ||
| width: 86px; | ||
| } | ||
| span { | ||
| background-color: var(--black3); | ||
| border-radius: 0 0 0.3rem 0.3rem; | ||
| color: var(--black9); | ||
| margin-left: 1.6rem; | ||
| } | ||
| button { | ||
| background-color: var(--brand); | ||
| border-radius: 0 0.3rem 0.3rem 0.3rem; | ||
| border-width: 0; | ||
| i { | ||
| padding: 0; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import ShellBox from './index'; | ||
| import type { Meta as MetaObj, StoryObj } from '@storybook/react'; | ||
| type Story = StoryObj<typeof ShellBox>; | ||
| type Meta = MetaObj<typeof ShellBox>; | ||
| export const Default: Story = { | ||
| args: { | ||
| children: 'echo hello world', | ||
| textToCopy: 'echo hello world', | ||
| }, | ||
| }; | ||
| export const WithoutTextToCopy: Story = { | ||
| args: { | ||
| children: 'echo hello world', | ||
| }, | ||
| }; | ||
| export const WithTextToCopyJsx: Story = { | ||
| args: { | ||
| children: ( | ||
| <span> | ||
| <strong>$</strong>echo hello world | ||
| </span> | ||
| ), | ||
| textToCopy: 'echo hello world', | ||
| }, | ||
| }; | ||
| export default { component: ShellBox } as Meta; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { useRef } from 'react'; | ||
| import { FormattedMessage } from 'react-intl'; | ||
| import styles from './index.module.scss'; | ||
| import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; | ||
| import type { FC, PropsWithChildren, MouseEvent, ReactNode } from 'react'; | ||
| type ShellBoxProps = { | ||
| children: string | ReactNode; | ||
ovflowd marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| textToCopy?: string; | ||
| }; | ||
| const ShellBox: FC<PropsWithChildren<ShellBoxProps>> = ({ | ||
| children, | ||
| textToCopy, | ||
| }: PropsWithChildren<ShellBoxProps>) => { | ||
| const [copied, copyText] = useCopyToClipboard(); | ||
| const shellBoxRef = useRef<HTMLElement>(null); | ||
| const handleCopyCode = async (event: MouseEvent<HTMLButtonElement>) => { | ||
| event.preventDefault(); | ||
| // By default we want to use the textToCopy props but if it's absent | ||
| // we allow the user to copy by getting the inner HTML content of the Element | ||
| const _textToCopy = textToCopy || shellBoxRef.current?.innerHTML || ''; | ||
| await copyText(_textToCopy.replace('$', '')); | ||
| }; | ||
| return ( | ||
| <pre className={styles.shellBox}> | ||
| <div className={styles.top}> | ||
| <span>SHELL</span> | ||
| <button type="button" onClick={handleCopyCode}> | ||
| <FormattedMessage | ||
| id="components.common.shellBox.copy" | ||
| values={{ copied }} | ||
| /> | ||
| </button> | ||
| </div> | ||
| <code ref={shellBoxRef}>{children}</code> | ||
| </pre> | ||
| ); | ||
| }; | ||
| export default ShellBox; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default as ShellBox } from './ShellBox'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { render, fireEvent, screen } from '@testing-library/react'; | ||
| import { FormattedMessage } from 'react-intl'; | ||
| import { IntlProvider } from 'react-intl'; | ||
| import { useCopyToClipboard } from '../useCopyToClipboard'; | ||
| const mockWriteText = jest.fn(); | ||
| const originalNavigator = { ...window.navigator }; | ||
| describe('useCopyToClipboard', () => { | ||
| beforeEach(() => { | ||
| Object.defineProperty(window, 'navigator', { | ||
| value: { | ||
| clipboard: { | ||
| writeText: mockWriteText, | ||
| }, | ||
| }, | ||
| }); | ||
| }); | ||
| afterEach(() => { | ||
| Object.defineProperty(window, 'navigator', { | ||
| value: originalNavigator, | ||
| }); | ||
| }); | ||
| const TestComponent = ({ textToCopy }: { textToCopy: string }) => { | ||
| const [copied, copyText] = useCopyToClipboard(); | ||
| return ( | ||
| <IntlProvider locale="en" onError={() => {}}> | ||
| <button onClick={() => copyText(textToCopy)} type="button"> | ||
| <FormattedMessage | ||
| id="components.common.shellBox.copy" | ||
| values={{ copied }} | ||
| /> | ||
| </button> | ||
| </IntlProvider> | ||
| ); | ||
| }; | ||
| it('should call clipboard API with `test` once', () => { | ||
| const navigatorClipboardWriteTextSpy = jest | ||
| .fn() | ||
| .mockImplementation(() => Promise.resolve()); | ||
| Object.defineProperty(window.navigator, 'clipboard', { | ||
| writable: true, | ||
| value: { | ||
| writeText: navigatorClipboardWriteTextSpy, | ||
| }, | ||
| }); | ||
| render(<TestComponent textToCopy="test" />); | ||
| const button = screen.getByRole('button'); | ||
| fireEvent.click(button); | ||
| expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1); | ||
| expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith('test'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { useEffect, useState } from 'react'; | ||
| const copyToClipboard = (value: string | undefined) => { | ||
| if (!value || typeof navigator === 'undefined') { | ||
| return Promise.resolve(false); | ||
| } | ||
| return navigator.clipboard | ||
| .writeText(value) | ||
| .then(() => true) | ||
| .catch(() => false); | ||
| }; | ||
| export const useCopyToClipboard = () => { | ||
| const [copied, setCopied] = useState(false); | ||
| const copyText = (text: string | undefined) => | ||
| copyToClipboard(text).then(setCopied); | ||
| useEffect(() => { | ||
| if (copied) { | ||
| const timerId = setTimeout(() => setCopied(false), 3000); | ||
AugustinMauroy marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return () => clearTimeout(timerId); | ||
| } | ||
| return undefined; | ||
AugustinMauroy marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }, [copied]); | ||
| return [copied, copyText] as const; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.