T23: Add dropdown outside-click and Escape close for RibbonBar
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import type { YjsDocument } from '../crdt/YjsDocument';
|
import type { YjsDocument } from '../crdt/YjsDocument';
|
||||||
import { exportDrawing, type ExportFormat } from '../services/exportService';
|
import { exportDrawing, type ExportFormat } from '../services/exportService';
|
||||||
import { importDrawing, type ImportFormat, type ImportResult } from '../services/importService';
|
import { importDrawing, type ImportFormat, type ImportResult } from '../services/importService';
|
||||||
@@ -19,6 +19,12 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const pendingImportFormat = useRef<ImportFormat>('json');
|
const pendingImportFormat = useRef<ImportFormat>('json');
|
||||||
|
|
||||||
|
// Refs for dropdown wrappers (for outside-click detection)
|
||||||
|
const exportDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const importDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
// Ref to track the trigger button that opened the print preview (for focus restoration)
|
||||||
|
const printTriggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'draw', label: 'Draw', icon: '✏️' },
|
{ id: 'draw', label: 'Draw', icon: '✏️' },
|
||||||
{ id: 'modify', label: 'Modify', icon: '🔧' },
|
{ id: 'modify', label: 'Modify', icon: '🔧' },
|
||||||
@@ -82,6 +88,40 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
|
|||||||
}
|
}
|
||||||
}, [yjsDoc]);
|
}, [yjsDoc]);
|
||||||
|
|
||||||
|
// Outside-click and Escape close for dropdowns
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as Node;
|
||||||
|
if (showExportMenu && exportDropdownRef.current && !exportDropdownRef.current.contains(target)) {
|
||||||
|
setShowExportMenu(false);
|
||||||
|
}
|
||||||
|
if (showImportMenu && importDropdownRef.current && !importDropdownRef.current.contains(target)) {
|
||||||
|
setShowImportMenu(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (showExportMenu) {
|
||||||
|
setShowExportMenu(false);
|
||||||
|
}
|
||||||
|
if (showImportMenu) {
|
||||||
|
setShowImportMenu(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showExportMenu || showImportMenu) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [showExportMenu, showImportMenu]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ribbon-bar" role="menubar" aria-label="CAD Functions">
|
<div className="ribbon-bar" role="menubar" aria-label="CAD Functions">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
@@ -99,7 +139,7 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
|
|||||||
|
|
||||||
<div className="ribbon-divider" aria-hidden="true" />
|
<div className="ribbon-divider" aria-hidden="true" />
|
||||||
|
|
||||||
<div className="ribbon-dropdown-wrapper">
|
<div className="ribbon-dropdown-wrapper" ref={exportDropdownRef}>
|
||||||
<button
|
<button
|
||||||
className="ribbon-tab"
|
className="ribbon-tab"
|
||||||
onClick={() => setShowExportMenu(!showExportMenu)}
|
onClick={() => setShowExportMenu(!showExportMenu)}
|
||||||
@@ -120,7 +160,7 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ribbon-dropdown-wrapper">
|
<div className="ribbon-dropdown-wrapper" ref={importDropdownRef}>
|
||||||
<button
|
<button
|
||||||
className="ribbon-tab"
|
className="ribbon-tab"
|
||||||
onClick={() => setShowImportMenu(!showImportMenu)}
|
onClick={() => setShowImportMenu(!showImportMenu)}
|
||||||
@@ -142,6 +182,7 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
|
|||||||
<div className="ribbon-divider" aria-hidden="true" />
|
<div className="ribbon-divider" aria-hidden="true" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
ref={printTriggerRef}
|
||||||
className="ribbon-tab"
|
className="ribbon-tab"
|
||||||
onClick={() => setShowPrintPreview(true)}
|
onClick={() => setShowPrintPreview(true)}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
@@ -163,7 +204,13 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ onTabChange, yjsDoc }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{showPrintPreview && yjsDoc && (
|
{showPrintPreview && yjsDoc && (
|
||||||
<PrintPreview yjsDoc={yjsDoc} onClose={() => setShowPrintPreview(false)} />
|
<PrintPreview yjsDoc={yjsDoc} onClose={() => {
|
||||||
|
setShowPrintPreview(false);
|
||||||
|
// Restore focus to the Print trigger button
|
||||||
|
if (printTriggerRef.current) {
|
||||||
|
printTriggerRef.current.focus();
|
||||||
|
}
|
||||||
|
}} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user