Skip to content

Commit

Permalink
Create TextSelectCopyMulti that allows multi lines and adding comments (
Browse files Browse the repository at this point in the history
#1194)

* Remove deprecated use of exec for copying to clipboard
* Rename existing Tabs to TabIcon
* Add and use @react/user-event library for testing clipboard
  • Loading branch information
kimlisa authored Sep 30, 2022
1 parent 07d6c29 commit 1e4bf2c
Show file tree
Hide file tree
Showing 14 changed files with 1,478 additions and 27 deletions.
1 change: 1 addition & 0 deletions web/packages/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@testing-library/jest-dom": "^5.15.1",
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^27.3.1",
"@types/lodash": "4.14.149",
"@types/node": "^16.11.10",
Expand Down
2 changes: 2 additions & 0 deletions web/packages/design/src/Icon/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const ChatBubble = makeFontIcon(
'ChatBubble',
'icon-chat_bubble_outline'
);
export const Check = makeFontIcon('Check', 'icon-check');
export const ChevronCircleDown = makeFontIcon(
'ChevronCircleDown',
'icon-chevron-down-circle'
Expand Down Expand Up @@ -124,6 +125,7 @@ export const Code = makeFontIcon('Code', 'icon-code');
export const Cog = makeFontIcon('Cog', 'icon-cog');
export const Config = makeFontIcon('Config', 'icon-config');
export const Contract = makeFontIcon('Contract', 'icon-frame-contract');
export const Copy = makeFontIcon('Copy', 'icon-copy');
export const CreditCard = makeFontIcon('CreditCard', 'icon-credit-card1');
export const CreditCardAlt = makeFontIcon(
'CreditCardAlt',
Expand Down
2 changes: 2 additions & 0 deletions web/packages/design/src/Icon/Icon.story.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.CarrotUp} text="CarrotUp" />
<IconBox IconCmpt={Icon.Cash} text="Cash" />
<IconBox IconCmpt={Icon.ChatBubble} text="ChatBubble" />
<IconBox IconCmpt={Icon.Check} text="Check" />
<IconBox IconCmpt={Icon.ChevronCircleDown} text="ChevronCircleDown" />
<IconBox IconCmpt={Icon.ChevronCircleLeft} text="ChevronCircleLeft" />
<IconBox IconCmpt={Icon.ChevronCircleRight} text="ChevronCircleRight" />
Expand Down Expand Up @@ -87,6 +88,7 @@ export const ListOfIcons = () => (
<IconBox IconCmpt={Icon.Cog} text="Cog" />
<IconBox IconCmpt={Icon.Config} text="Config" />
<IconBox IconCmpt={Icon.Contract} text="Contract" />
<IconBox IconCmpt={Icon.Copy} text="Copy" />
<IconBox IconCmpt={Icon.CreditCard} text="CreditCard" />
<IconBox IconCmpt={Icon.CreditCardAlt} text="CreditCardAlt" />
<IconBox IconCmpt={Icon.CreditCardAlt2} text="CreditCardAlt2" />
Expand Down
24 changes: 0 additions & 24 deletions web/packages/design/src/utils/copyToClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,8 @@
* @param textToCopy the text to copy to clipboard
*/
export default function copyToClipboard(textToCopy: string): Promise<any> {
// DELETE when navigator.clipboard is not a working draft
if (fallbackCopyToClipboard(textToCopy)) {
return Promise.resolve();
}

return navigator.clipboard.writeText(textToCopy).catch(err => {
// This can happen if the user denies clipboard permissions:
window.prompt('Cannot copy to clipboard. Use ctrl/cmd + c', err);
});
}

/**
* fallbackCopyToClipboard is used when navigator.clipboard is not supported.
* Note: document.execCommand is marked obselete.
*
* @param textToCopy the text to copy to clipboard
*/
function fallbackCopyToClipboard(textToCopy: string): boolean {
let aux = document.createElement('textarea');
aux.value = textToCopy;
document.body.appendChild(aux);
aux.select();

// returns false if the command is not supported or enabled
let isSuccess = document.execCommand('copy');
document.body.removeChild(aux);

return isSuccess;
}
2 changes: 2 additions & 0 deletions web/packages/design/src/utils/testing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
prettyDOM,
getByTestId,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter as Router } from 'react-router-dom';

import ThemeProvider from 'design/ThemeProvider';
Expand Down Expand Up @@ -58,4 +59,5 @@ export {
waitFor,
getByTestId,
Router,
userEvent,
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import React from 'react';

import Component from './TextSelectCopy';
import { TextSelectCopy as Component } from './TextSelectCopy';

export default {
title: 'Teleport/TextSelectCopy',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import selectElementContent from 'design/utils/selectElementContent';
import { ButtonPrimary, Box, Flex } from 'design';
import { useTheme } from 'styled-components';

export default function TextSelectCopy({
export function TextSelectCopy({
text,
fontFamily,
allowMultiline,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright 2022 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from 'react';

import { render } from 'design/utils/testing';

import * as stories from './TextSelectCopyMulti.story';

test('render multi bash texts', () => {
const { container } = render(<stories.BashMulti />);
expect(container).toMatchSnapshot();
});

test('render multi bash texts with comment', () => {
const { container } = render(<stories.BashMultiWithComment />);
expect(container).toMatchSnapshot();
});

test('render single bash text', () => {
const { container } = render(<stories.BashSingle />);
expect(container).toMatchSnapshot();
});

test('render single bash text with comment', () => {
const { container } = render(<stories.BashSingleWithComment />);
expect(container).toMatchSnapshot();
});

test('render non bash single text', () => {
const { container } = render(<stories.NonBash />);
expect(container).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Copyright 2022 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from 'react';

import { TextSelectCopyMulti as Component } from './TextSelectCopyMulti';

export default {
title: 'Teleport/TextSelectCopy/Multi',
};

export const BashMulti = () => {
return (
<Component
lines={[
{
text: `sudo tctl -c cfg-all users add --roles=access,editor george_washington`,
},
{
text: 'sudo DEBUG=1 teleport start -c cfg-all -d',
},
{
text: 'yarn start-teleport-e --target=https://localhost:3080/web',
},
]}
/>
);
};

export const BashMultiWithComment = () => {
return (
<Component
lines={[
{
text: `sudo curl https://apt.releases.teleport.dev/gpg \\\n-o /usr/share/keyrings/teleport-archive-keyring.asc`,
comment: `Download Teleport's PGP public key`,
},
{
text: 'sudo DEBUG=1 teleport start -c cfg-all -d',
},
{
text:
`echo "deb [signed-by=/usr/share/keyrings/teleport-archive-keyring.asc] \\\n` +
`https://apt.releases.teleport.dev/stable/v10" \\\n` +
`| sudo tee /etc/apt/sources.list.d/teleport.list > /dev/null`,
comment:
`Add the Teleport APT repository for v10. You'll need to update this` +
`\nfile for each major release of Teleport.\n` +
`Note: if using a fork of Debian or Ubuntu you may need to use '$ID_LIKE'\n` +
`and the codename your distro was forked from instead of '$ID' and '$VERSION_CODENAME'.\n`,
},
]}
/>
);
};

export const BashSingle = () => {
return (
<Component
lines={[
{
text: `sudo tctl -c cfg-all users add --roles=access,editor george_washington`,
},
]}
/>
);
};

export const BashSingleWithComment = () => {
return (
<Component
lines={[
{
text: `sudo tctl -c cfg-all users add --roles=access,editor george_washington`,
comment: `Add the Teleport API repository for v10. You'll need to update this.`,
},
]}
/>
);
};

export const NonBash = () => {
return (
<Component
lines={[
{
text: 'some -c text to be copied and it is super long to test scrolling',
},
]}
bash={false}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright 2022 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from 'react';

import { render, screen, userEvent } from 'design/utils/testing';

import { TextSelectCopyMulti } from './TextSelectCopyMulti';

jest.useFakeTimers();

test('changing of icon when button is clicked', async () => {
const user = userEvent.setup({ delay: null });
render(<TextSelectCopyMulti lines={[{ text: 'some text to copy' }]} />);

// Init button states.
expect(screen.queryByTestId('btn-copy')).toBeVisible();
expect(screen.queryByTestId('btn-check')).not.toBeVisible();

// Clicking copy button should change the button icon to "check".
await user.click(screen.getByTestId('btn-copy'));
expect(screen.getByTestId('btn-check')).toBeVisible();
expect(screen.getByTestId('btn-copy')).not.toBeVisible();

const clipboardText = await navigator.clipboard.readText();
expect(clipboardText).toBe('some text to copy');

// After set time out, the buttons should return to its initial state.
jest.runAllTimers();
expect(screen.queryByTestId('btn-copy')).toBeVisible();
expect(screen.queryByTestId('btn-check')).not.toBeVisible();
});

test('correct copying of texts', async () => {
const user = userEvent.setup({ delay: null });
render(
<TextSelectCopyMulti
lines={[
{ text: 'text to copy1', comment: 'first comment' },
{ text: 'text to copy2', comment: 'second comment' },
]}
/>
);

// Test copying of each text.
const btns = screen.queryAllByRole('button');
expect(btns).toHaveLength(2);

await user.click(btns[1]);
let clipboardText = await navigator.clipboard.readText();
expect(clipboardText).toBe('text to copy2');

await user.click(btns[0]);
clipboardText = await navigator.clipboard.readText();
expect(clipboardText).toBe('text to copy1');
});
Loading

0 comments on commit 1e4bf2c

Please sign in to comment.