T23: Add dropdown outside-click and Escape close for RibbonBar

This commit is contained in:
2026-06-22 23:45:04 +00:00
parent 04c93adff3
commit d2edc31460
+51 -4
View File
@@ -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>
); );