🐛 fix(sidebar): 修复 v2 搜索关闭交互异常

- 关闭命令搜索前提交同步筛选值,避免输入框清空覆盖侧栏筛选

- 限制弹窗打开期间才同步命令输入到侧栏持久筛选

- 增加全局 ESC 关闭监听,修复焦点离开弹窗后无法关闭

- 补充回归测试覆盖筛选保留和全局 ESC 关闭规则
This commit is contained in:
Syngnat
2026-06-02 17:10:31 +08:00
parent 3eb9fd0acb
commit dd8af73887
2 changed files with 118 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ import Sidebar, {
hasSidebarLazyChildren,
normalizeSidebarTreeRelativeDropPosition,
parseV2CommandSearchQuery,
resolveV2CommandSearchPersistentFilter,
type V2CommandSearchItem,
resolveSidebarDropNodeFromDomEvent,
resolveSidebarTagDropInsertBefore,
@@ -27,6 +28,7 @@ import Sidebar, {
shouldSkipSidebarLoadOnExpandWhileDragging,
shouldSkipSidebarSelectWhileDragging,
shouldLoadSidebarNodeOnExpand,
shouldCloseV2CommandSearchOnGlobalKey,
shouldRunV2CommandSearchEnter,
sortSidebarTableEntries,
} from './Sidebar';
@@ -302,6 +304,51 @@ describe('Sidebar locate toolbar', () => {
})).toBe(false);
});
it('keeps v2 command search persisted filter after closing the palette', () => {
expect(resolveV2CommandSearchPersistentFilter({
commandSearchValue: ' org ',
persistedFilter: '',
enabled: true,
isOpen: true,
})).toBe('org');
expect(resolveV2CommandSearchPersistentFilter({
commandSearchValue: '',
persistedFilter: 'org',
enabled: true,
isOpen: false,
})).toBe('org');
expect(resolveV2CommandSearchPersistentFilter({
commandSearchValue: 'org',
persistedFilter: 'org',
enabled: false,
isOpen: true,
})).toBe('');
});
it('closes v2 command search on global escape only while the palette is open', () => {
expect(shouldCloseV2CommandSearchOnGlobalKey({
key: 'Escape',
isOpen: true,
})).toBe(true);
expect(shouldCloseV2CommandSearchOnGlobalKey({
key: 'Esc',
isOpen: true,
})).toBe(true);
expect(shouldCloseV2CommandSearchOnGlobalKey({
key: 'Escape',
isOpen: false,
})).toBe(false);
expect(shouldCloseV2CommandSearchOnGlobalKey({
key: 'Enter',
isOpen: true,
})).toBe(false);
});
it('keeps all loaded v2 command table matches once a keyword is entered', () => {
const items: V2CommandSearchItem[] = Array.from({ length: 40 }, (_, index) => ({
key: `node-table-${index}`,
@@ -538,6 +585,8 @@ describe('Sidebar locate toolbar', () => {
expect(source).toContain('handleV2CommandSearchValueChange(event.target.value)');
expect(source).toContain('toggleV2CommandSearchPersistentFilter');
expect(source).toContain('gn-v2-command-filter-switch');
expect(source).toContain("window.addEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true)");
expect(source).toContain("window.removeEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true)");
expect(source).toContain('onClick={() => setV2ExplorerFilter(item.key)}');
expect(source).toContain('treeData={isV2Ui ? v2VisibleTreeData : displayTreeData}');
expect(markup).toContain('gn-v2-sidebar-log-footer');

View File

@@ -648,6 +648,38 @@ export const shouldRunV2CommandSearchEnter = ({
return activeItemCount > 0;
};
export interface V2CommandSearchPersistentFilterState {
commandSearchValue: string;
persistedFilter: string;
enabled: boolean;
isOpen: boolean;
}
export const resolveV2CommandSearchPersistentFilter = ({
commandSearchValue,
persistedFilter,
enabled,
isOpen,
}: V2CommandSearchPersistentFilterState): string => {
if (!enabled) return '';
if (!isOpen) return String(persistedFilter ?? '').trim();
return String(commandSearchValue ?? '').trim();
};
export interface V2CommandSearchGlobalKeyState {
key: string;
isOpen: boolean;
}
export const shouldCloseV2CommandSearchOnGlobalKey = ({
key,
isOpen,
}: V2CommandSearchGlobalKeyState): boolean => {
if (!isOpen) return false;
const normalizedKey = String(key || '').toLowerCase();
return normalizedKey === 'escape' || normalizedKey === 'esc';
};
export const resolveSidebarConnectionIdFromKey = (
key: unknown,
connectionIds: string[],
@@ -1156,11 +1188,23 @@ const Sidebar: React.FC<{
setV2CommandActiveIndex(0);
}, []);
const commitV2CommandSearchPersistentFilter = useCallback((value = v2CommandSearchValue) => {
if (!v2CommandSearchPersistentFilterEnabled) {
return;
}
const nextFilter = value.trim();
setSearchValue(nextFilter);
if (nextFilter !== v2PersistedSidebarFilter) {
setAppearance({ v2SidebarPersistedFilter: nextFilter });
}
}, [setAppearance, v2CommandSearchPersistentFilterEnabled, v2CommandSearchValue, v2PersistedSidebarFilter]);
const closeV2CommandSearch = useCallback(() => {
commitV2CommandSearchPersistentFilter();
setIsV2CommandSearchOpen(false);
setV2CommandSearchValue('');
setV2CommandActiveIndex(0);
}, []);
}, [commitV2CommandSearchPersistentFilter]);
useEffect(() => {
setSearchValue(v2PersistedSidebarFilter);
@@ -1184,13 +1228,21 @@ const Sidebar: React.FC<{
if (!v2CommandSearchPersistentFilterEnabled) {
return;
}
const nextFilter = deferredV2CommandSearchValue.trim();
if (!isV2CommandSearchOpen) {
return;
}
const nextFilter = resolveV2CommandSearchPersistentFilter({
commandSearchValue: deferredV2CommandSearchValue,
persistedFilter: v2PersistedSidebarFilter,
enabled: v2CommandSearchPersistentFilterEnabled,
isOpen: isV2CommandSearchOpen,
});
setSearchValue(nextFilter);
const timer = window.setTimeout(() => {
setAppearance({ v2SidebarPersistedFilter: nextFilter });
}, 160);
return () => window.clearTimeout(timer);
}, [deferredV2CommandSearchValue, setAppearance, v2CommandSearchPersistentFilterEnabled]);
}, [deferredV2CommandSearchValue, isV2CommandSearchOpen, setAppearance, v2CommandSearchPersistentFilterEnabled, v2PersistedSidebarFilter]);
const toggleV2CommandSearchPersistentFilter = useCallback((enabled: boolean) => {
const nextFilter = enabled ? v2CommandSearchValue.trim() : '';
@@ -1264,6 +1316,20 @@ const Sidebar: React.FC<{
}, 0);
return () => window.clearTimeout(timer);
}, [isV2CommandSearchOpen]);
useEffect(() => {
if (!isV2CommandSearchOpen) return;
const handleV2CommandSearchGlobalKeyDown = (event: KeyboardEvent) => {
if (!shouldCloseV2CommandSearchOnGlobalKey({ key: event.key, isOpen: isV2CommandSearchOpen })) {
return;
}
event.preventDefault();
event.stopPropagation();
closeV2CommandSearch();
};
window.addEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true);
return () => window.removeEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true);
}, [closeV2CommandSearch, isV2CommandSearchOpen]);
// Connection Status State: key -> 'success' | 'error'
const [connectionStates, setConnectionStates] = useState<Record<string, 'success' | 'error'>>({});