Skip to content

Commit 22538bf

Browse files
committed
change
1 parent 859d956 commit 22538bf

File tree

3 files changed

+50
-34
lines changed

3 files changed

+50
-34
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "feat: add Copy as Markdown button to FluentDocsPage",
4+
"packageName": "@fluentui/react-storybook-addon",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-storybook-addon/package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
"dependencies": {
1919
"@fluentui/react-aria": "^9.17.0",
2020
"@fluentui/react-button": "^9.6.6",
21-
"@fluentui/react-menu": "^9.19.6",
21+
"@fluentui/react-icons": "^2.0.245",
2222
"@fluentui/react-label": "^9.3.5",
23-
"@fluentui/react-switch": "^9.4.5",
24-
"@fluentui/react-text": "^9.6.5",
2523
"@fluentui/react-link": "^9.6.5",
24+
"@fluentui/react-menu": "^9.19.6",
2625
"@fluentui/react-provider": "^9.22.5",
27-
"@fluentui/react-utilities": "^9.24.1",
26+
"@fluentui/react-shared-contexts": "^9.24.0",
27+
"@fluentui/react-spinner": "^9.6.5",
28+
"@fluentui/react-switch": "^9.4.5",
29+
"@fluentui/react-text": "^9.6.5",
2830
"@fluentui/react-theme": "^9.2.0",
31+
"@fluentui/react-toast": "^9.7.1",
32+
"@fluentui/react-utilities": "^9.24.1",
2933
"@griffel/react": "^1.5.22",
3034
"@swc/helpers": "^0.5.1"
3135
},

packages/react-components/react-storybook-addon/src/docs/CopyAsMarkdownButton.tsx

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
import * as React from 'react';
2-
import {
3-
makeStyles,
4-
Menu,
5-
MenuButtonProps,
6-
MenuItem,
7-
MenuList,
8-
MenuPopover,
9-
MenuTrigger,
10-
Spinner,
11-
SplitButton,
12-
Toast,
13-
Toaster,
14-
ToastTitle,
15-
useId,
16-
useToastController,
17-
} from '@fluentui/react-components';
2+
import { SplitButton, type MenuButtonProps } from '@fluentui/react-button';
3+
import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from '@fluentui/react-menu';
4+
import { Spinner } from '@fluentui/react-spinner';
5+
import { Toast, Toaster, ToastTitle, useToastController } from '@fluentui/react-toast';
6+
import { useId } from '@fluentui/react-utilities';
7+
import { makeStyles } from '@griffel/react';
188
import { bundleIcon, MarkdownFilled, MarkdownRegular } from '@fluentui/react-icons';
9+
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
1910

2011
const MarkdownIcon = bundleIcon(MarkdownFilled, MarkdownRegular);
2112

@@ -35,6 +26,8 @@ export interface CopyAsMarkdownProps {
3526
* The markdown content is fetched from the Storybook API and cached for subsequent requests.
3627
*/
3728
export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId = '' }) => {
29+
const { targetDocument } = useFluent();
30+
const targetWindow = targetDocument?.defaultView;
3831
const styles = useStyles();
3932
const toastId = useId('copy-toast');
4033
const toasterId = useId('toaster');
@@ -48,8 +41,8 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
4841

4942
// Full URL to the markdown endpoint for this story
5043
const markdownUrl = React.useMemo(() => {
51-
return convertStoryIdToMarkdownUrl(storyId);
52-
}, [storyId]);
44+
return targetWindow ? convertStoryIdToMarkdownUrl(targetWindow, storyId) : '';
45+
}, [storyId, targetWindow]);
5346

5447
// Cleanup: abort pending requests on unmount
5548
React.useEffect(() => {
@@ -69,6 +62,11 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
6962
return;
7063
}
7164

65+
// Ensure we have a window context to use for clipboard and fetch
66+
if (!targetWindow) {
67+
return;
68+
}
69+
7270
// Create new AbortController for this request
7371
const abortController = new AbortController();
7472
abortControllerRef.current = abortController;
@@ -88,11 +86,11 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
8886
try {
8987
// Use cached content if available, otherwise fetch from API
9088
if (!markdownContentCache.current) {
91-
markdownContentCache.current = await fetchMarkdownContent(markdownUrl, abortController.signal);
89+
markdownContentCache.current = await fetchMarkdownContent(targetWindow, markdownUrl, abortController.signal);
9290
}
9391

9492
// Copy to clipboard
95-
await navigator.clipboard.writeText(markdownContentCache.current);
93+
await targetWindow?.navigator.clipboard.writeText(markdownContentCache.current);
9694

9795
// Update toast to success
9896
updateToast({
@@ -128,12 +126,12 @@ export const CopyAsMarkdownButton: React.FC<CopyAsMarkdownProps> = ({ storyId =
128126
// Clear the abort controller ref to allow new requests
129127
abortControllerRef.current = null;
130128
}
131-
}, [dispatchToast, updateToast, toastId, markdownUrl]);
129+
}, [dispatchToast, updateToast, toastId, markdownUrl, targetWindow]);
132130

133131
/** Opens the markdown content in a new browser tab */
134132
const openInNewTab = React.useCallback(() => {
135-
window.open(markdownUrl, '_blank');
136-
}, [markdownUrl]);
133+
targetWindow?.open(markdownUrl, '_blank');
134+
}, [markdownUrl, targetWindow]);
137135

138136
if (!storyId) {
139137
return null;
@@ -182,33 +180,40 @@ const STORYBOOK_VARIANT_SUFFIX_PATTERN = /--\w+$/g;
182180
/**
183181
* Gets the base URL for fetching markdown content from the Storybook LLM endpoint.
184182
* Each story's markdown is available at: {BASE_URL}/{storyId}.txt
183+
* @param targetWindow - The window object to use for location access
185184
* @returns The base URL constructed from current location origin and pathname
186185
*/
187-
function getStorybookMarkdownApiBaseUrl(): string {
186+
function getStorybookMarkdownApiBaseUrl(targetWindow: Window): string {
188187
// Remove the [page].html file from pathname and append /llms/
189-
const basePath = window.location.pathname.replace(/\/[^/]*\.html$/, '');
190-
return `${window.location.origin}${basePath}/llms/`;
188+
const basePath = targetWindow.location.pathname.replace(/\/[^/]*\.html$/, '');
189+
return `${targetWindow.location.origin}${basePath}/llms/`;
191190
}
192191

193192
/**
194193
* Converts a Storybook story ID to a markdown URL.
194+
* @param targetWindow - The window object to use for location access
195195
* @param storyId - The Storybook story ID
196196
* @returns The full URL to the markdown endpoint for the story
197197
* @example "button--primary" -> "https://storybooks.fluentui.dev/llms/button.txt"
198198
*/
199-
function convertStoryIdToMarkdownUrl(storyId: string): string {
200-
return `${getStorybookMarkdownApiBaseUrl()}${storyId.replace(STORYBOOK_VARIANT_SUFFIX_PATTERN, '.txt')}`;
199+
function convertStoryIdToMarkdownUrl(targetWindow: Window, storyId: string): string {
200+
return `${getStorybookMarkdownApiBaseUrl(targetWindow)}${storyId.replace(STORYBOOK_VARIANT_SUFFIX_PATTERN, '.txt')}`;
201201
}
202202

203203
/**
204204
* Fetches markdown content from the Storybook API.
205+
* @param targetWindow - The window object to use for fetch access
205206
* @param url - The URL to fetch markdown content from
206207
* @param signal - Optional AbortSignal to cancel the request
207208
* @returns Promise resolving to the markdown text content
208209
* @throws Error if the fetch request fails or is aborted
209210
*/
210-
async function fetchMarkdownContent(url: string, signal?: AbortSignal): Promise<string> {
211-
const response = await fetch(url, {
211+
async function fetchMarkdownContent(
212+
targetWindow: Window,
213+
url: string,
214+
signal: AbortSignal | undefined,
215+
): Promise<string> {
216+
const response = await targetWindow.fetch(url, {
212217
headers: {
213218
'Content-Type': 'text/plain',
214219
},

0 commit comments

Comments
 (0)