Skip to content

Commit

Permalink
Copy button to use outerHTML/textContent
Browse files Browse the repository at this point in the history
  • Loading branch information
compulim committed Nov 14, 2024
1 parent f4bb6a9 commit d232420
Show file tree
Hide file tree
Showing 38 changed files with 70 additions and 68 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 0 additions & 3 deletions __tests__/html/copyButton.denied.js

This file was deleted.

3 changes: 0 additions & 3 deletions __tests__/html/copyButton.disabled.js

This file was deleted.

3 changes: 0 additions & 3 deletions __tests__/html/copyButton.hideAndShow.js

This file was deleted.

3 changes: 0 additions & 3 deletions __tests__/html/copyButton.js

This file was deleted.

12 changes: 0 additions & 12 deletions __tests__/html/copyButton.layout.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@
expect(copyButton.getAttribute('aria-disabled')).toBe('true');

// THEN: Should match screenshot.
await host.snapshot();
await host.snapshot('local');

// WHEN: Clicks the "Copy" button.
await host.click(copyButton);

// THEN: The copy button should not change to "Copied".
await host.snapshot();
await host.snapshot('local');
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@
expect(copyButton.getAttribute('aria-disabled')).toBe('true');

// THEN: Should match screenshot.
await host.snapshot();
await host.snapshot('local');

// WHEN: Clicks the "Copy" button.
await host.click(copyButton);

// THEN: The copy button should not change to "Copied".
await host.snapshot();
await host.snapshot('local');
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,21 @@
await host.click(document.querySelector(`[data-testid="${WebChat.testIds.copyButton}"]`));

// THEN: The "Copy" button should say "Copied".
await host.snapshot();
await host.snapshot('local');

// WHEN: After 1 second.
await testHelpers.sleep(1_000);

// THEN: The "Copy" button should back to normal.
await host.snapshot();
await host.snapshot('local');

// WHEN: Hiding Web Chat and showing it back.
document.getElementById('webchat').style.display = 'none';
document.body.offsetWidth; // Need for browser to refresh the layout.
document.getElementById('webchat').style.display = '';

// THEN: The "Copy" button should kept at normal.
await host.snapshot();
await host.snapshot('local');
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
type: 'https://schema.org/Message'
}
],
text: 'Mollit *aute* **aute** dolor ea ex magna incididunt nostrud sit nisi.',
text: 'Mollit *aute* **aute** dolor ea ex magna incididunt nostrud sit nisi. ![Icon](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAYAAABiS3YzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAFvSURBVHgB7ZPdcYJAFIUX9D2UgBWIFQS0AUsgFYQOhA7sILGCkAIUUkGwA0og7/zku8mSWYHM+OKbd8bhsufcc84uq1L3unn5vu+u1+u3K7lSu+G6PVywLCtr2/asrqsC/hbh6EJj4Bzatv14Op2eSLvrum6VZdlWjRO6Yg4e8OowI0EWeZ5Xo6QQnwEPMsRryFA0FY/hEu47vxd6SVsgHKrh9hFyAF1IOYTodzYvDdzTZj+FeQxf1hzMDywtR6Lz+dwDPOvES0k8OJZP2bKRtoIvph5YSb8YidZ1XQE+6BRfs9nM6TH6Spsps+BXaqLMMxU3Vw8XTdN4PXA8HlMEAsxW5jB84RQEkrk/gwvrzWaTIZaIgWxXRMxzNUtuBw9XbkoQBClBUvrXYVJx/gDcaaGEfj8lKB+NR4hpoq/XshcciQLEPBz5l9DvMYmnROUa6aOo9MdLTNyaSOGy9ZQhv7/M/xVHEMFz4MXqXjetb0P8xVy6y7nkAAAAAElFTkSuQmCC)',
type: 'message'
});

Expand All @@ -88,13 +88,13 @@
const copyButton = document.querySelector(`[data-testid="${WebChat.testIds.copyButton}"]`);

expect(document.activeElement).toBe(copyButton);
await host.snapshot();
await host.snapshot('local');

// WHEN: Press ENTER on the "Copy" button.
await host.sendKeys('ENTER');

// THEN: The copy button should change to "Copied".
await host.snapshot();
await host.snapshot('local');

// WHEN: Paste into plain text and rich text text box.
await host.click(document.querySelector('[data-testid="plain-text-box"]'));
Expand All @@ -109,7 +109,7 @@
await testHelpers.sleep(500);

// THEN: Plain text box should contains plain text, while rich text box should contains rich text.
await host.snapshot();
await host.snapshot('local');
});
</script>
</body>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions __tests__/html2/copyButton/layout.copilot.dark.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
window.location.href = './layout?theme=dark&variant=copilot';
</script>
</head>
<body></body>
</html>
9 changes: 9 additions & 0 deletions __tests__/html2/copyButton/layout.copilot.light.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
window.location.href = './layout?theme=light&variant=copilot';
</script>
</head>
<body></body>
</html>
9 changes: 9 additions & 0 deletions __tests__/html2/copyButton/layout.fluent.dark.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
window.location.href = './layout?theme=dark&variant=fluent';
</script>
</head>
<body></body>
</html>
9 changes: 9 additions & 0 deletions __tests__/html2/copyButton/layout.fluent.light.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
window.location.href = './layout?theme=light&variant=fluent';
</script>
</head>
<body></body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@

const root = createRoot(document.getElementById('webchat'));

if (searchParams.get('variant') === 'white label') {
root.render(<App />);
} else {
if (searchParams.get('variant') === 'copilot' || searchParams.get('variant') === 'fluent') {
root.render(
<FluentProvider
theme={
Expand All @@ -73,6 +71,8 @@
</FluentThemeProvider>
</FluentProvider>
);
} else {
root.render(<App />);
}

await pageConditions.uiConnected();
Expand Down Expand Up @@ -139,7 +139,7 @@
);

// THEN: "Copy" button should appear after the message.
await host.snapshot();
await host.snapshot('local');
});
</script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion docs/HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,7 @@ This function is for rendering the avatar of an activity. The caller will need t
<!-- prettier-ignore-start -->
```js
useRenderMarkdownAsHTML(
mode: 'accessible name' | 'adaptive cards' | 'citation modal' | 'clipboard' | 'message activity' = 'message activity'
mode: 'accessible name' | 'adaptive cards' | 'citation modal' | 'message activity' = 'message activity'
): (markdown: string): string
```
<!-- prettier-ignore-end -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { hooks } from 'botframework-webchat-api';
import classNames from 'classnames';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useRefFrom } from 'use-ref-from';
import React, { memo, useCallback, useEffect, useRef, useState, type RefObject } from 'react';
import useStyleSet from '../../../hooks/useStyleSet';
import ActivityButton from './ActivityButton';

const { useLocalizer, useUIState } = hooks;

type Props = Readonly<{
className?: string | undefined;
htmlText?: string | undefined;
plainText: string;
targetRef?: RefObject<HTMLElement>;
}>;

const COPY_ICON_URL = `data:image/svg+xml;utf8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none"><path d="M8.5 2C7.39543 2 6.5 2.89543 6.5 4V14C6.5 15.1046 7.39543 16 8.5 16H14.5C15.6046 16 16.5 15.1046 16.5 14V4C16.5 2.89543 15.6046 2 14.5 2H8.5ZM7.5 4C7.5 3.44772 7.94772 3 8.5 3H14.5C15.0523 3 15.5 3.44772 15.5 4V14C15.5 14.5523 15.0523 15 14.5 15H8.5C7.94772 15 7.5 14.5523 7.5 14V4ZM4.5 6.00001C4.5 5.25973 4.9022 4.61339 5.5 4.26758V14.5C5.5 15.8807 6.61929 17 8 17H14.2324C13.8866 17.5978 13.2403 18 12.5 18H8C6.067 18 4.5 16.433 4.5 14.5V6.00001Z" fill="#000000"/></svg>')}`;

const ActivityCopyButton = ({ className, htmlText, plainText }: Props) => {
const ActivityCopyButton = ({ className, targetRef }: Props) => {
const [{ activityButton, activityCopyButton }] = useStyleSet();
const [permissionGranted, setPermissionGranted] = useState(false);
const [uiState] = useUIState();
const buttonRef = useRef<HTMLButtonElement>(null);
const htmlTextRef = useRefFrom(htmlText);
const localize = useLocalizer();
const plainTextRef = useRefFrom(plainText);

const copiedText = localize('COPY_BUTTON_COPIED_TEXT');
const copyText = localize('COPY_BUTTON_TEXT');
Expand All @@ -41,13 +37,14 @@ const ActivityCopyButton = ({ className, htmlText, plainText }: Props) => {
}, [buttonRef]);

const handleClick = useCallback(() => {
const { current: htmlText } = htmlTextRef;
const htmlText = targetRef.current?.outerHTML;
const plainText = targetRef.current?.textContent;

navigator.clipboard
?.write([
new ClipboardItem({
...(htmlText ? { 'text/html': new Blob([htmlText], { type: 'text/html' }) } : {}),
'text/plain': new Blob([plainTextRef.current], { type: 'text/plain' })
...(plainText ? { 'text/plain': new Blob([plainText], { type: 'text/plain' }) } : {})
})
])
.catch(error => console.error(`botframework-webchat-fluent-theme: Failed to copy to clipboard.`, error));
Expand All @@ -60,7 +57,7 @@ const ActivityCopyButton = ({ className, htmlText, plainText }: Props) => {
buttonRef.current?.offsetWidth;

buttonRef.current?.classList.add('webchat__activity-copy-button--copied');
}, [buttonRef, htmlTextRef, plainTextRef]);
}, [buttonRef, targetRef]);

useEffect(() => {
let unmounted = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { Fragment, memo } from 'react';
import React, { Fragment, memo, useMemo } from 'react';

import useRenderMarkdownAsHTML from '../../../hooks/useRenderMarkdownAsHTML';
import useStyleSet from '../../../hooks/useStyleSet';
Expand All @@ -13,6 +13,8 @@ const CitationModalContent = memo(({ headerText, markdown }: Props) => {
const [{ renderMarkdown: renderMarkdownStyleSet }] = useStyleSet();
const renderMarkdownAsHTML = useRenderMarkdownAsHTML('citation modal');

const html = useMemo(() => ({ __html: renderMarkdownAsHTML(markdown) }), [markdown, renderMarkdownAsHTML]);

return (
<Fragment>
{headerText && <h2 className="webchat__citation-modal-dialog__header">{headerText}</h2>}
Expand All @@ -25,7 +27,7 @@ const CitationModalContent = memo(({ headerText, markdown }: Props) => {
)}
// The content rendered by `renderMarkdownAsHTML` is sanitized.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: renderMarkdownAsHTML(markdown) }}
dangerouslySetInnerHTML={html}
/>
) : (
<div className={classNames('webchat__render-markdown', renderMarkdownStyleSet + '')}>{markdown}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import classNames from 'classnames';
import type { Definition } from 'mdast';
import { fromMarkdown } from 'mdast-util-from-markdown';
import React, { memo, useCallback, useMemo, type MouseEventHandler, type ReactNode } from 'react';
import React, { memo, useCallback, useMemo, useRef, type MouseEventHandler, type ReactNode } from 'react';
import { useRefFrom } from 'use-ref-from';

import { LinkDefinitionItem, LinkDefinitions } from '../../../LinkDefinition/index';
Expand Down Expand Up @@ -54,10 +54,10 @@ const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => {
textContent: textContentStyleSet
}
] = useStyleSet();
const contentRef = useRef<HTMLDivElement>(null);
const localize = useLocalizer();
const graph = useMemo(() => dereferenceBlankNodes(activity.entities || []), [activity.entities]);
const renderMarkdownAsHTML = useRenderMarkdownAsHTML('message activity');
const renderMarkdownAsHTMLForClipboard = useRenderMarkdownAsHTML('clipboard');
const showModal = useShowModal();

const messageThing = useMemo(() => getOrgSchemaMessage(graph), [graph]);
Expand All @@ -73,11 +73,6 @@ const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => {
[renderMarkdownAsHTML, markdown]
);

const htmlTextForClipboard = useMemo(
() => (markdown ? renderMarkdownAsHTMLForClipboard(markdown) : undefined),
[markdown, renderMarkdownAsHTMLForClipboard]
);

const markdownDefinitions = useMemo(
() => fromMarkdown(markdown).children.filter((node): node is Definition => node.type === 'definition'),
[markdown]
Expand Down Expand Up @@ -215,6 +210,7 @@ const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => {
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={dangerouslySetInnerHTML}
onClick={handleClick}
ref={contentRef}
/>
{children}
{!!entries.length && (
Expand Down Expand Up @@ -246,11 +242,7 @@ const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => {
/>
) : null}
{activity.type === 'message' && activity.text && messageThing?.keywords?.includes('AllowCopy') ? (
<ActivityCopyButton
className="webchat__text-content__activity-copy-button"
htmlText={htmlTextForClipboard}
plainText={activity.text}
/>
<ActivityCopyButton className="webchat__text-content__activity-copy-button" targetRef={contentRef} />
) : null}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/component/src/LiveRegion/LiveRegionActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const LiveRegionActivity: VFC<LiveRegionActivityProps> = ({ activity }) => {
const fallbackText: string | undefined =
type === 'message' ? activity.channelData['webchat:fallback-text'] : undefined;
const localize = useLocalizer();
const renderMarkdownAsHTML = useRenderMarkdownAsHTML();
const renderMarkdownAsHTML = useRenderMarkdownAsHTML('accessible name');
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
const textAlt = useMemo(() => activityAltText(activity, renderMarkdownAsHTML), [activity, renderMarkdownAsHTML]);

Expand Down
5 changes: 2 additions & 3 deletions packages/component/src/hooks/useRenderMarkdownAsHTML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import useStyleSet from './useStyleSet';
const { useLocalizer, useStyleOptions } = hooks;

export default function useRenderMarkdownAsHTML(
mode: 'accessible name' | 'adaptive cards' | 'citation modal' | 'clipboard' | 'message activity' = 'message activity'
mode: 'accessible name' | 'adaptive cards' | 'citation modal' | 'message activity' = 'message activity'
):
| ((
markdown: string,
Expand All @@ -34,9 +34,8 @@ export default function useRenderMarkdownAsHTML(
{
'webchat__render-markdown--adaptive-cards': mode === 'adaptive cards',
'webchat__render-markdown--citation': mode === 'citation modal',
'webchat__render-markdown--clipboard': mode === 'clipboard',
'webchat__render-markdown--message-activity':
mode !== 'accessible name' && mode !== 'adaptive cards' && mode !== 'citation modal' && mode !== 'clipboard'
mode !== 'accessible name' && mode !== 'adaptive cards' && mode !== 'citation modal'
},
renderMarkdownStyleSet + ''
),
Expand Down

0 comments on commit d232420

Please sign in to comment.