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

UI: Allow keyboard shortcut to copy code in preview blocks #15559

Merged
merged 7 commits into from Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 42 additions & 7 deletions lib/components/src/blocks/Preview.tsx
@@ -1,13 +1,22 @@
import React, { Children, FunctionComponent, ReactElement, ReactNode, useState } from 'react';
import React, {
Children,
ClipboardEvent,
FunctionComponent,
ReactElement,
ReactNode,
useState,
} from 'react';
import { darken } from 'polished';
import { styled } from '@storybook/theming';

import global from 'global';
import { getBlockBackgroundStyle } from './BlockBackgroundStyles';
import { Source, SourceProps } from './Source';
import { ActionBar, ActionItem } from '../ActionBar/ActionBar';
import { Toolbar } from './Toolbar';
import { ZoomContext } from './ZoomContext';
import { Zoom } from '../Zoom/Zoom';
import { createCopyToClipboardFunction } from '../syntaxhighlighter/syntaxhighlighter';

export interface PreviewProps {
isColumn?: boolean;
Expand Down Expand Up @@ -130,7 +139,7 @@ const getSource = (
}
default: {
return {
source: null,
source: <StyledSource {...withSource} dark />,
actionItem: {
title: 'Show code',
className: 'docblock-code-toggle',
Expand Down Expand Up @@ -197,13 +206,39 @@ const Preview: FunctionComponent<PreviewProps> = ({
const previewClasses = [className].concat(['sbdocs', 'sbdocs-preview']);

const defaultActionItems = withSource ? [actionItem] : [];
const actionItems = additionalActions
? [...defaultActionItems, ...additionalActions]
: defaultActionItems;
const [additionalActionItems, setAdditionalActionItems] = useState(
additionalActions ? [...additionalActions] : []
);
const actionItems = [...defaultActionItems, ...additionalActionItems];

// @ts-ignore
const layout = getLayout(Children.count(children) === 1 ? [children] : children);

const { window: globalWindow } = global;
const copyToClipboard: (text: string) => Promise<void> = createCopyToClipboardFunction();

const onCopyCapture = (e: ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
if (additionalActionItems.filter((item) => item.title === 'Copied').length === 0) {
copyToClipboard(source.props.code).then(() => {
setAdditionalActionItems([
...additionalActionItems,
{
title: 'Copied',
onClick: () => {},
},
]);
globalWindow.setTimeout(
() =>
setAdditionalActionItems(
additionalActionItems.filter((item) => item.title !== 'Copied')
),
1500
);
});
}
};

return (
<PreviewContainer
{...{ withSource, withToolbar }}
Expand All @@ -220,7 +255,7 @@ const Preview: FunctionComponent<PreviewProps> = ({
/>
)}
<ZoomContext.Provider value={{ scale }}>
<Relative className="docs-story">
<Relative className="docs-story" onCopyCapture={withSource && onCopyCapture}>
<ChildrenContainer
isColumn={isColumn || !Array.isArray(children)}
columns={columns}
Expand All @@ -238,7 +273,7 @@ const Preview: FunctionComponent<PreviewProps> = ({
<ActionBar actionItems={actionItems} />
</Relative>
</ZoomContext.Provider>
{withSource && source}
{withSource && expanded && source}
</PreviewContainer>
);
};
Expand Down
31 changes: 22 additions & 9 deletions lib/components/src/syntaxhighlighter/syntaxhighlighter.tsx
@@ -1,4 +1,10 @@
import React, { ComponentProps, FunctionComponent, MouseEvent, useState } from 'react';
import React, {
ClipboardEvent,
ComponentProps,
FunctionComponent,
MouseEvent,
useState,
} from 'react';
import { logger } from '@storybook/client-logger';
import { styled } from '@storybook/theming';
import global from 'global';
Expand Down Expand Up @@ -54,12 +60,13 @@ const themedSyntax = memoize(2)((theme) =>
Object.entries(theme.code || {}).reduce((acc, [key, val]) => ({ ...acc, [`* .${key}`]: val }), {})
);

let copyToClipboard: (text: string) => Promise<void>;
const copyToClipboard: (text: string) => Promise<void> = createCopyToClipboardFunction();

if (navigator?.clipboard) {
copyToClipboard = (text: string) => navigator.clipboard.writeText(text);
} else {
copyToClipboard = async (text: string) => {
export function createCopyToClipboardFunction() {
if (navigator?.clipboard) {
return (text: string) => navigator.clipboard.writeText(text);
}
return async (text: string) => {
const tmp = document.createElement('TEXTAREA');
const focus = document.activeElement;

Expand All @@ -72,6 +79,7 @@ if (navigator?.clipboard) {
focus.focus();
};
}

export interface WrapperProps {
bordered?: boolean;
padded?: boolean;
Expand Down Expand Up @@ -152,10 +160,15 @@ export const SyntaxHighlighter: FunctionComponent<Props> = ({
const highlightableCode = format ? formatter(children) : children.trim();
const [copied, setCopied] = useState(false);

const onClick = (e: MouseEvent<HTMLButtonElement>) => {
const onClick = (e: MouseEvent<HTMLButtonElement> | ClipboardEvent<HTMLDivElement>) => {
e.preventDefault();

copyToClipboard(highlightableCode)
const textToCopy =
e.type !== 'click' && globalWindow.getSelection().toString() !== ''
? globalWindow.getSelection().toString()
: highlightableCode;
darleendenno marked this conversation as resolved.
Show resolved Hide resolved

copyToClipboard(textToCopy)
.then(() => {
setCopied(true);
globalWindow.setTimeout(() => setCopied(false), 1500);
Expand All @@ -164,7 +177,7 @@ export const SyntaxHighlighter: FunctionComponent<Props> = ({
};

return (
<Wrapper bordered={bordered} padded={padded} className={className}>
<Wrapper bordered={bordered} padded={padded} className={className} onCopyCapture={onClick}>
<Scroller>
<ReactSyntaxHighlighter
padded={padded || bordered}
Expand Down