Skip to content

Commit c610b08

Browse files
committed
0.6.2: fix first dropdown placement
1 parent dae8f70 commit c610b08

3 files changed

Lines changed: 64 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## 0.6.2 — 2026-05-22
8+
9+
### Fixed
10+
11+
- Dropdown positioning is calculated before the menu first renders, preventing the first interaction from briefly showing the menu in the viewport's top-left corner.
12+
713
## 0.6.1 — 2026-05-21
814

915
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "docusaurus-plugin-copy-page-button",
3-
"version": "0.6.1",
3+
"version": "0.6.2",
44
"description": "Docusaurus plugin that adds a copy page button to extract documentation content as markdown for AI tools like ChatGPT, Claude, Perplexity, and Gemini",
55
"main": "src/index.js",
66
"files": [

src/CopyPageButton.js

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ const {
88
const DEFAULT_ACTIONS = ['copy', 'view', 'chatgpt', 'claude', 'perplexity', 'gemini'];
99
const DEFAULT_MCP_ACTIONS = ['mcp-copy', 'mcp-cursor', 'mcp-vscode'];
1010
const POSITIONING_PROPS = ['position', 'top', 'right', 'bottom', 'left', 'zIndex', 'transform'];
11+
const DROPDOWN_WIDTH = 300;
12+
const DROPDOWN_OFFSET = 8;
13+
const VIEWPORT_PADDING = 8;
1114

1215
// Utility function to merge custom styles with default classes
1316
const mergeStyles = (defaultClassName, customStyleConfig = {}) => {
@@ -39,6 +42,19 @@ const separatePositioningStyles = (styleObject = {}) => {
3942
return { positioning, nonPositioning };
4043
};
4144

45+
const getDropdownPosition = (buttonElement) => {
46+
const rect = buttonElement.getBoundingClientRect();
47+
const maxLeft = window.innerWidth - DROPDOWN_WIDTH - VIEWPORT_PADDING;
48+
49+
return {
50+
top: rect.bottom + DROPDOWN_OFFSET,
51+
left: Math.max(
52+
VIEWPORT_PADDING,
53+
Math.min(rect.right - DROPDOWN_WIDTH, maxLeft)
54+
),
55+
};
56+
};
57+
4258
export default function CopyPageButton({
4359
customStyles = {},
4460
enabledActions,
@@ -47,7 +63,7 @@ export default function CopyPageButton({
4763
}) {
4864
const [isOpen, setIsOpen] = useState(false);
4965
const [pageContent, setPageContent] = useState("");
50-
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
66+
const [dropdownPosition, setDropdownPosition] = useState(null);
5167
const dropdownRef = useRef(null);
5268
const buttonRef = useRef(null);
5369

@@ -57,6 +73,30 @@ export default function CopyPageButton({
5773
const dropdownStyleConfig = customStyles.dropdown || {};
5874
const dropdownItemStyleConfig = customStyles.dropdownItem || {};
5975

76+
const updateDropdownPosition = () => {
77+
if (typeof window === 'undefined' || !buttonRef.current) {
78+
return false;
79+
}
80+
81+
setDropdownPosition(getDropdownPosition(buttonRef.current));
82+
return true;
83+
};
84+
85+
const closeDropdown = () => {
86+
setIsOpen(false);
87+
setDropdownPosition(null);
88+
};
89+
90+
const handleButtonClick = () => {
91+
if (isOpen) {
92+
closeDropdown();
93+
return;
94+
}
95+
96+
updateDropdownPosition();
97+
setIsOpen(true);
98+
};
99+
60100
useEffect(() => {
61101
const handleClickOutside = (event) => {
62102
if (
@@ -65,7 +105,7 @@ export default function CopyPageButton({
65105
buttonRef.current &&
66106
!buttonRef.current.contains(event.target)
67107
) {
68-
setIsOpen(false);
108+
closeDropdown();
69109
}
70110
};
71111

@@ -79,19 +119,19 @@ export default function CopyPageButton({
79119
}, [isOpen]);
80120

81121
useEffect(() => {
82-
if (isOpen && buttonRef.current) {
83-
const rect = buttonRef.current.getBoundingClientRect();
84-
const dropdownWidth = 300;
85-
const viewportPadding = 8;
86-
const maxLeft = window.innerWidth - dropdownWidth - viewportPadding;
87-
setDropdownPosition({
88-
top: rect.bottom + 8,
89-
left: Math.max(
90-
viewportPadding,
91-
Math.min(rect.right - dropdownWidth, maxLeft)
92-
),
93-
});
122+
if (!isOpen || typeof window === 'undefined') {
123+
return undefined;
94124
}
125+
126+
updateDropdownPosition();
127+
128+
window.addEventListener('resize', updateDropdownPosition);
129+
window.addEventListener('scroll', updateDropdownPosition, true);
130+
131+
return () => {
132+
window.removeEventListener('resize', updateDropdownPosition);
133+
window.removeEventListener('scroll', updateDropdownPosition, true);
134+
};
95135
}, [isOpen]);
96136

97137
useEffect(() => {
@@ -470,7 +510,7 @@ Please provide a clear summary and help me understand the key concepts covered i
470510
<button
471511
className={buttonProps.className}
472512
style={buttonProps.style}
473-
onClick={() => setIsOpen(!isOpen)}
513+
onClick={handleButtonClick}
474514
aria-expanded={isOpen}
475515
aria-haspopup="true"
476516
ref={buttonRef}
@@ -503,7 +543,7 @@ Please provide a clear summary and help me understand the key concepts covered i
503543
</button>
504544
</div>
505545

506-
{isOpen && (
546+
{isOpen && dropdownPosition && (
507547
<div
508548
className={dropdownProps.className}
509549
style={{
@@ -522,7 +562,7 @@ Please provide a clear summary and help me understand the key concepts covered i
522562
style={dropdownItemProps.style}
523563
onClick={() => {
524564
item.action();
525-
setIsOpen(false);
565+
closeDropdown();
526566
}}
527567
>
528568
{item.icon}

0 commit comments

Comments
 (0)