mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-09 16:23:27 +08:00
🐛 fix(sidebar): 调整折叠状态下的展开按钮位置
- 将 V2 侧栏展开按钮从标题栏移至固定工具栏顶部 - 保持展开与折叠后的焦点回迁和无障碍属性 - 补充固定栏按钮顺序、交互和样式回归测试
This commit is contained in:
@@ -30,6 +30,7 @@ describe('app sidebar tree panel collapse', () => {
|
||||
expect(appSource).toContain('data-sidebar-collapse-trigger="true"');
|
||||
expect(appSource).toContain('data-titlebar-brand-region="true"');
|
||||
expect(appSource).toContain('data-sidebar-toggle-placement="titlebar"');
|
||||
expect(connectionRailSource).toContain('data-sidebar-toggle-placement="fixed-rail"');
|
||||
expect(appSource).toContain('data-no-titlebar-toggle="true"');
|
||||
expect(appSource).toContain('aria-controls="gonavi-sidebar-tree-panel"');
|
||||
expect(appSource).toContain('aria-expanded={!isSidebarCollapsed}');
|
||||
@@ -40,13 +41,15 @@ describe('app sidebar tree panel collapse', () => {
|
||||
expect(appSource).toContain("case 'focusSidebarSearch':");
|
||||
expect(appSource).toContain('handleFocusSidebarSearch();');
|
||||
expect(appSource).toContain('<TabManager onFocusSidebarSearch={handleFocusSidebarSearch} />');
|
||||
expect(appSource).toContain('{(!isV2Ui || isSidebarCollapsed) && (');
|
||||
expect(appSource).toContain('{!isV2Ui && (');
|
||||
expect(appSource).toContain('onCollapseSidebar={isV2Ui && !isSidebarCollapsed ? handleCollapseSidebarPanel : undefined}');
|
||||
expect(appSource).toContain('onExpandSidebar={isV2Ui && isSidebarCollapsed ? handleExpandSidebarPanel : undefined}');
|
||||
expect(appSource).toContain('collapseSidebarButtonRef={sidebarExplorerToggleRef}');
|
||||
expect(appSource).toContain('ref={sidebarTitlebarToggleRef}');
|
||||
expect(appSource).toContain("pendingSidebarToggleFocusRef.current = 'titlebar'");
|
||||
expect(appSource).toContain('expandSidebarButtonRef={sidebarCollapsedToggleRef}');
|
||||
expect(appSource).toContain('ref={sidebarCollapsedToggleRef}');
|
||||
expect(appSource).toContain("pendingSidebarToggleFocusRef.current = 'collapsed'");
|
||||
expect(appSource).toContain("pendingSidebarToggleFocusRef.current = 'explorer'");
|
||||
expect(appSource).toContain("(target === 'titlebar' ? sidebarTitlebarToggleRef : sidebarExplorerToggleRef).current?.focus()");
|
||||
expect(appSource).toContain("(target === 'collapsed' ? sidebarCollapsedToggleRef : sidebarExplorerToggleRef).current?.focus()");
|
||||
expect(sidebarSource).toContain('data-sidebar-toggle-placement="explorer-header"');
|
||||
|
||||
const titlebarToggleIndex = appSource.indexOf('data-sidebar-collapse-trigger="true"');
|
||||
@@ -64,6 +67,10 @@ describe('app sidebar tree panel collapse', () => {
|
||||
const explorerToggleEndIndex = sidebarSource.indexOf('</Tooltip>', explorerToggleIndex);
|
||||
const explorerToggleStartIndex = sidebarSource.lastIndexOf('<Button', explorerToggleIndex);
|
||||
const explorerToggleSource = sidebarSource.slice(explorerToggleStartIndex, explorerToggleEndIndex);
|
||||
const fixedRailToggleIndex = connectionRailSource.indexOf('data-sidebar-toggle-placement="fixed-rail"');
|
||||
const railItemsIndex = connectionRailSource.indexOf('<div className="gn-v2-rail-items">');
|
||||
const railPrimaryActionsIndex = connectionRailSource.indexOf('<div className="gn-v2-rail-primary-actions"');
|
||||
const firstRailObjectActionIndex = connectionRailSource.indexOf('data-sidebar-create-group-action="true"');
|
||||
expect(titlebarToggleIndex).toBeGreaterThan(appSource.indexOf('data-titlebar-brand-region="true"'));
|
||||
expect(titlebarToggleIndex).toBeLessThan(siderIndex);
|
||||
expect(triggerSource).toContain('type="text"');
|
||||
@@ -75,6 +82,10 @@ describe('app sidebar tree panel collapse', () => {
|
||||
expect(explorerToggleIndex).toBeGreaterThan(connectionMenuIndex);
|
||||
expect(explorerToggleSource).toContain('ref={collapseSidebarButtonRef}');
|
||||
expect(explorerToggleSource).not.toContain('disabled=');
|
||||
expect(fixedRailToggleIndex).toBeGreaterThan(-1);
|
||||
expect(fixedRailToggleIndex).toBeGreaterThan(railItemsIndex);
|
||||
expect(fixedRailToggleIndex).toBeGreaterThan(railPrimaryActionsIndex);
|
||||
expect(fixedRailToggleIndex).toBeLessThan(firstRailObjectActionIndex);
|
||||
});
|
||||
|
||||
it('overrides normal resize bounds with the retained rail width and removes the collapsed resize target', () => {
|
||||
|
||||
@@ -1073,24 +1073,25 @@ function App() {
|
||||
const sidebarWidth = useStore(state => state.sidebarWidth);
|
||||
const setSidebarWidth = useStore(state => state.setSidebarWidth);
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
const sidebarTitlebarToggleRef = useRef<HTMLButtonElement>(null);
|
||||
const sidebarCollapsedToggleRef = useRef<HTMLButtonElement>(null);
|
||||
const sidebarExplorerToggleRef = useRef<HTMLButtonElement>(null);
|
||||
const pendingSidebarToggleFocusRef = useRef<'titlebar' | 'explorer' | null>(null);
|
||||
const pendingSidebarToggleFocusRef = useRef<'collapsed' | 'explorer' | null>(null);
|
||||
const handleCollapseSidebarPanel = useCallback(() => {
|
||||
pendingSidebarToggleFocusRef.current = 'titlebar';
|
||||
pendingSidebarToggleFocusRef.current = 'collapsed';
|
||||
setIsSidebarCollapsed(true);
|
||||
}, []);
|
||||
const handleExpandSidebarPanel = useCallback(() => {
|
||||
pendingSidebarToggleFocusRef.current = 'explorer';
|
||||
setIsSidebarCollapsed(false);
|
||||
}, []);
|
||||
const handleTitlebarSidebarToggle = useCallback(() => {
|
||||
if (isV2Ui && isSidebarCollapsed) {
|
||||
pendingSidebarToggleFocusRef.current = 'explorer';
|
||||
}
|
||||
setIsSidebarCollapsed((collapsed) => !collapsed);
|
||||
}, [isSidebarCollapsed, isV2Ui]);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const target = pendingSidebarToggleFocusRef.current;
|
||||
if (!target) return;
|
||||
pendingSidebarToggleFocusRef.current = null;
|
||||
(target === 'titlebar' ? sidebarTitlebarToggleRef : sidebarExplorerToggleRef).current?.focus();
|
||||
(target === 'collapsed' ? sidebarCollapsedToggleRef : sidebarExplorerToggleRef).current?.focus();
|
||||
}, [isSidebarCollapsed]);
|
||||
const sidebarCollapsedWidth = isV2Ui ? 38 * effectiveUiScale * effectiveSidebarRailScale : 0;
|
||||
const renderedSidebarWidth = isSidebarCollapsed ? sidebarCollapsedWidth : sidebarWidth;
|
||||
@@ -7522,10 +7523,10 @@ function App() {
|
||||
}}
|
||||
/>
|
||||
<span>GoNavi</span>
|
||||
{(!isV2Ui || isSidebarCollapsed) && (
|
||||
{!isV2Ui && (
|
||||
<Tooltip title={sidebarPanelToggleLabel} placement="bottom" mouseEnterDelay={0.35}>
|
||||
<Button
|
||||
ref={sidebarTitlebarToggleRef}
|
||||
ref={sidebarCollapsedToggleRef}
|
||||
type="text"
|
||||
size="small"
|
||||
className="gonavi-sidebar-collapse-trigger"
|
||||
@@ -7665,8 +7666,11 @@ function App() {
|
||||
uiVersion={appearance.uiVersion}
|
||||
onFocusCommandSearch={handleFocusSidebarSearch}
|
||||
onCollapseSidebar={isV2Ui && !isSidebarCollapsed ? handleCollapseSidebarPanel : undefined}
|
||||
onExpandSidebar={isV2Ui && isSidebarCollapsed ? handleExpandSidebarPanel : undefined}
|
||||
collapseSidebarLabel={sidebarPanelToggleLabel}
|
||||
collapseSidebarButtonRef={sidebarExplorerToggleRef}
|
||||
expandSidebarLabel={sidebarPanelToggleLabel}
|
||||
expandSidebarButtonRef={sidebarCollapsedToggleRef}
|
||||
isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -511,8 +511,11 @@ const Sidebar: React.FC<{
|
||||
uiVersion?: 'legacy' | 'v2';
|
||||
onFocusCommandSearch?: () => void;
|
||||
onCollapseSidebar?: () => void;
|
||||
onExpandSidebar?: () => void;
|
||||
collapseSidebarLabel?: string;
|
||||
collapseSidebarButtonRef?: React.Ref<HTMLButtonElement>;
|
||||
expandSidebarLabel?: string;
|
||||
expandSidebarButtonRef?: React.Ref<HTMLButtonElement>;
|
||||
isTreePanelCollapsed?: boolean;
|
||||
}> = React.memo(({
|
||||
onCreateConnection,
|
||||
@@ -523,8 +526,11 @@ const Sidebar: React.FC<{
|
||||
uiVersion,
|
||||
onFocusCommandSearch,
|
||||
onCollapseSidebar,
|
||||
onExpandSidebar,
|
||||
collapseSidebarLabel,
|
||||
collapseSidebarButtonRef,
|
||||
expandSidebarLabel,
|
||||
expandSidebarButtonRef,
|
||||
isTreePanelCollapsed = false,
|
||||
}) => {
|
||||
const connections = useStore(state => state.connections);
|
||||
@@ -3007,6 +3013,11 @@ const Sidebar: React.FC<{
|
||||
openSettings: onOpenSettings ?? (() => {}),
|
||||
},
|
||||
canLocateActiveTab,
|
||||
sidebarExpandAction: onExpandSidebar && expandSidebarLabel ? {
|
||||
label: expandSidebarLabel,
|
||||
onClick: onExpandSidebar,
|
||||
buttonRef: expandSidebarButtonRef,
|
||||
} : undefined,
|
||||
workbenchActions: (
|
||||
<>
|
||||
<SlowQueryRailButton
|
||||
|
||||
@@ -16,6 +16,7 @@ vi.mock('@ant-design/icons', () => {
|
||||
FileAddOutlined: Icon,
|
||||
FolderOpenOutlined: Icon,
|
||||
ImportOutlined: Icon,
|
||||
MenuUnfoldOutlined: Icon,
|
||||
RobotOutlined: Icon,
|
||||
SettingOutlined: Icon,
|
||||
TableOutlined: Icon,
|
||||
@@ -23,6 +24,55 @@ vi.mock('@ant-design/icons', () => {
|
||||
});
|
||||
|
||||
describe('SidebarConnectionRail', () => {
|
||||
it('renders the collapsed sidebar toggle at the top of the fixed rail', () => {
|
||||
const expandSidebar = vi.fn();
|
||||
const noop = vi.fn();
|
||||
const renderer = create(
|
||||
<SidebarConnectionRail
|
||||
labels={{
|
||||
railSystemActions: 'System actions',
|
||||
railObjectActions: 'Object actions',
|
||||
newGroup: 'New group',
|
||||
batchTables: 'Batch tables',
|
||||
batchDatabases: 'Batch databases',
|
||||
dataImport: 'Data import',
|
||||
openExternalSqlFile: 'Open SQL file',
|
||||
locateCurrentTable: 'Locate table',
|
||||
locateCurrentTableUnavailable: 'No table',
|
||||
aiAssistant: 'AI assistant',
|
||||
settings: 'Settings',
|
||||
}}
|
||||
handlers={{
|
||||
openCreateTagModal: noop,
|
||||
openBatchTableExport: noop,
|
||||
openBatchDatabaseExport: noop,
|
||||
openDataImport: noop,
|
||||
openExternalSqlFile: noop,
|
||||
locateActiveTab: noop,
|
||||
toggleAI: noop,
|
||||
openSettings: noop,
|
||||
}}
|
||||
canLocateActiveTab
|
||||
sidebarExpandAction={{ label: 'Expand sidebar', onClick: expandSidebar }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const rail = renderer.root.findByProps({ 'data-sidebar-fixed-rail': 'true' });
|
||||
const primaryActions = rail.findByProps({ className: 'gn-v2-rail-primary-actions' });
|
||||
const toggleSlot = rail.findByProps({ className: 'gn-v2-rail-sidebar-toggle-slot' });
|
||||
const toggle = rail.findByProps({ 'data-sidebar-toggle-placement': 'fixed-rail' });
|
||||
expect(primaryActions.children.indexOf(toggleSlot)).toBe(0);
|
||||
expect(primaryActions.findAllByType('button').slice(0, 2).map((button) => button.props['aria-label'])).toEqual([
|
||||
'Expand sidebar',
|
||||
'New group',
|
||||
]);
|
||||
expect(toggle.props['aria-label']).toBe('Expand sidebar');
|
||||
expect(toggle.props['aria-expanded']).toBe(false);
|
||||
|
||||
toggle.props.onClick();
|
||||
expect(expandSidebar).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('opens the data import workbench from its dedicated rail action', () => {
|
||||
const openDataImport = vi.fn();
|
||||
const noop = vi.fn();
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ImportOutlined,
|
||||
FileAddOutlined,
|
||||
AimOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
RobotOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
@@ -45,13 +46,37 @@ export interface SidebarConnectionRailProps {
|
||||
openSettings: () => void;
|
||||
};
|
||||
canLocateActiveTab: boolean;
|
||||
sidebarExpandAction?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
buttonRef?: React.Ref<HTMLButtonElement>;
|
||||
};
|
||||
workbenchActions?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SidebarConnectionRail: React.FC<SidebarConnectionRailProps> = ({ labels, handlers, canLocateActiveTab, workbenchActions }) => (
|
||||
const SidebarConnectionRail: React.FC<SidebarConnectionRailProps> = ({ labels, handlers, canLocateActiveTab, sidebarExpandAction, workbenchActions }) => (
|
||||
<div className="gn-v2-connection-rail" data-sidebar-fixed-rail="true" aria-label={labels.railSystemActions}>
|
||||
<div className="gn-v2-rail-items">
|
||||
<div className="gn-v2-rail-primary-actions" aria-label={labels.railObjectActions}>
|
||||
{sidebarExpandAction && (
|
||||
<div className="gn-v2-rail-sidebar-toggle-slot">
|
||||
<Tooltip title={sidebarExpandAction.label} placement="right" mouseEnterDelay={0.35}>
|
||||
<button
|
||||
ref={sidebarExpandAction.buttonRef}
|
||||
type="button"
|
||||
className="gn-v2-rail-tool gn-v2-rail-sidebar-toggle"
|
||||
data-sidebar-collapse-trigger="true"
|
||||
data-sidebar-toggle-placement="fixed-rail"
|
||||
aria-label={sidebarExpandAction.label}
|
||||
aria-controls="gonavi-sidebar-tree-panel"
|
||||
aria-expanded={false}
|
||||
onClick={sidebarExpandAction.onClick}
|
||||
>
|
||||
<MenuUnfoldOutlined />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip title={labels.newGroup} placement="right">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1820,6 +1820,18 @@ body[data-ui-version="v2"] .gn-v2-connection-rail {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-rail-sidebar-toggle-slot {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-rail-sidebar-toggle:focus-visible {
|
||||
outline: 2px solid var(--gn-accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-rail-items {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
Reference in New Issue
Block a user