Skip to content

Commit 5ceea89

Browse files
fixes #329: Add multi-format clipboard support to useCopyToClipboard
1 parent 945436d commit 5ceea89

File tree

3 files changed

+65
-14
lines changed

3 files changed

+65
-14
lines changed

index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ export type SpeechState = {
116116
volume: number;
117117
};
118118

119+
export type ClipboardFormats = {
120+
plain?: string;
121+
html?: string;
122+
markdown?: string;
123+
};
124+
119125
declare module "@uidotdev/usehooks" {
120126
export function useBattery(): BatteryManager;
121127

@@ -125,7 +131,7 @@ declare module "@uidotdev/usehooks" {
125131

126132
export function useCopyToClipboard(): [
127133
string | null,
128-
(value: string) => Promise<void>
134+
(value: string | ClipboardFormats) => void
129135
];
130136

131137
export function useCounter(

index.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,32 @@ export function useCopyToClipboard() {
145145
const copyToClipboard = React.useCallback((value) => {
146146
const handleCopy = async () => {
147147
try {
148-
if (navigator?.clipboard?.writeText) {
148+
// ClipboardItem API for multiple formats
149+
if (typeof value === 'object' && value !== null && window.ClipboardItem) {
150+
const items = {};
151+
if (value.plain) {
152+
items["text/plain"] = new Blob([value.plain], { type: "text/plain" });
153+
}
154+
if (value.html) {
155+
items["text/html"] = new Blob([value.html], { type: "text/html" });
156+
}
157+
if (value.markdown) {
158+
items["text/markdown"] = new Blob([value.markdown], { type: "text/markdown" });
159+
}
160+
const clipboardItem = new ClipboardItem(items);
161+
await navigator.clipboard.write([clipboardItem]);
162+
setState(value.plain || value.html || value.markdown);
163+
} else if (navigator?.clipboard?.writeText) {
149164
await navigator.clipboard.writeText(value);
150165
setState(value);
151166
} else {
152167
throw new Error("writeText not supported");
153168
}
154169
} catch (e) {
155-
oldSchoolCopy(value);
156-
setState(value);
170+
oldSchoolCopy(typeof value === 'object' ? value.plain : value);
171+
setState(typeof value === 'object' ? value.plain : value);
157172
}
158173
};
159-
160174
handleCopy();
161175
}, []);
162176

@@ -653,10 +667,27 @@ export function useLocalStorage(key, initialValue) {
653667

654668
export function useLockBodyScroll() {
655669
React.useLayoutEffect(() => {
656-
const originalStyle = window.getComputedStyle(document.body).overflow;
670+
// Preserve the current scroll position
671+
const scrollY = window.scrollY || window.pageYOffset;
672+
673+
// Save original inline styles so we can restore them
674+
const originalOverflow = window.getComputedStyle(document.body).overflow;
675+
const originalPosition = document.body.style.position;
676+
const originalTop = document.body.style.top;
677+
678+
// Lock the body scroll by fixing position and offsetting by the scroll
657679
document.body.style.overflow = "hidden";
680+
document.body.style.position = "fixed";
681+
document.body.style.top = `-${scrollY}px`;
682+
658683
return () => {
659-
document.body.style.overflow = originalStyle;
684+
// Restore original inline styles
685+
document.body.style.position = originalPosition;
686+
document.body.style.top = originalTop;
687+
document.body.style.overflow = originalOverflow;
688+
689+
// Restore the scroll position
690+
window.scrollTo(0, scrollY);
660691
};
661692
}, []);
662693
}

usehooks.com/src/content/hooks/useLockBodyScroll.mdx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import HookDescription from "../../components/HookDescription.astro";
1313
import StaticCodeContainer from "../../components/StaticCodeContainer.astro";
1414

1515
<HookDescription name={frontmatter.name}>
16-
The useLockBodyScroll hook temporarily disables scrolling on the document
17-
body. This can be beneficial in scenarios where you want to restrict scrolling
18-
while displaying a modal, a dropdown menu, or any other component that
19-
requires the user’s focus. Once the component using this hook is unmounted or
20-
no longer needed, the hook returns a cleanup function that restores the
21-
original overflow style, ensuring that the scroll behavior is reverted to its
22-
previous state.
16+
The `useLockBodyScroll` hook temporarily disables scrolling on the document
17+
body. Instead of only toggling the `overflow` style (which can sometimes
18+
cause the viewport to jump to the top on some browsers/layouts), this hook
19+
preserves the user's current scroll position and locks the page by applying
20+
a fixed position and an offset. When the hook cleans up it restores the
21+
original body styles and returns the user to the exact same scroll position.
22+
23+
This is useful when showing modals, dropdowns, or any UI that should keep
24+
focus without changing the page scroll.
2325
</HookDescription>
2426

2527
<CodePreview
@@ -83,3 +85,15 @@ export default function App() {
8385
```
8486

8587
</StaticCodeContainer>
88+
89+
### Notes
90+
91+
- This implementation preserves the scroll position by setting `position: fixed` and
92+
`top: -<scrollY>px` on the `body`, then restoring the original styles and
93+
calling `window.scrollTo(0, scrollY)` on cleanup.
94+
- If your app can open nested modals or multiple components that call
95+
`useLockBodyScroll` at the same time, consider a reference-counted lock so the
96+
body styles are only restored when the last lock is released.
97+
- For advanced use cases or mobile-specific edge cases, you may prefer a
98+
battle-tested library such as `body-scroll-lock` or `disable-scroll`.
99+

0 commit comments

Comments
 (0)