Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions webapp/packages/core-app/src/AppScreen/AppScreenBootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
Expand All @@ -10,15 +10,19 @@ import { Executor, type IExecutor } from '@cloudbeaver/core-executor';
import { ScreenService } from '@cloudbeaver/core-routing';

import { AppScreenService } from './AppScreenService.js';
import { SkipNavService } from './SkipNavService.js';
import { importLazyComponent } from '@cloudbeaver/core-blocks';

const AppScreen = importLazyComponent(() => import('./AppScreen.js').then(m => m.AppScreen));

@injectable(() => [ScreenService])
@injectable(() => [ScreenService, SkipNavService])
export class AppScreenBootstrap extends Bootstrap {
readonly activation: IExecutor;

constructor(private readonly screenService: ScreenService) {
constructor(
private readonly screenService: ScreenService,
private readonly skipNavService: SkipNavService,
) {
super();
this.activation = new Executor();
}
Expand All @@ -33,5 +37,7 @@ export class AppScreenBootstrap extends Bootstrap {
await this.activation.execute();
},
});

this.skipNavService.registerLinks();
}
}
6 changes: 5 additions & 1 deletion webapp/packages/core-app/src/AppScreen/AppScreenService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { injectable } from '@cloudbeaver/core-di';

export const PANEL_ID_LEFT_SIDEBAR = 'dbeaver-left-sidebar';
export const PANEL_ID_RIGHT_SIDEBAR = 'dbeaver-right-sidebar';
export const PANEL_ID_MAIN_CONTENT = 'dbeaver-main-content';

@injectable()
export class AppScreenService {
static screenName = 'app';
Expand Down
5 changes: 3 additions & 2 deletions webapp/packages/core-app/src/AppScreen/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { useService } from '@cloudbeaver/core-di';
import { LeftBarPanelService, SideBarPanel, SideBarPanelService, TabStyles } from '@cloudbeaver/core-ui';

import { PANEL_ID_LEFT_SIDEBAR, PANEL_ID_RIGHT_SIDEBAR } from './AppScreenService.js';
import { RightArea } from './RightArea.js';
import style from './Main.module.css';
import LeftSideBarPanel from './LeftSideBarPanel.module.css';
Expand Down Expand Up @@ -67,7 +68,7 @@ export const Main = observer(function Main() {
<Pane className={s(styles, { pane: true })} basis="250px" main>
<Loader suspense>
<SContext registry={LEFT_SIDEBAR_PANEL_REGISTRY}>
<SideBarPanel container={leftBarPanelService.tabsContainer} panelId="dbeaver-left-sidebar" />
<SideBarPanel container={leftBarPanelService.tabsContainer} panelId={PANEL_ID_LEFT_SIDEBAR} />
</SContext>
</Loader>
</Pane>
Expand All @@ -80,7 +81,7 @@ export const Main = observer(function Main() {
<ResizerControls />
<Pane className={s(styles, { pane: true })} basis="400px" main>
<Loader className={s(styles, { loader: true })} suspense>
<SideBarPanel container={sideBarPanelService.tabsContainer} panelId="dbeaver-right-sidebar" />
<SideBarPanel container={sideBarPanelService.tabsContainer} panelId={PANEL_ID_RIGHT_SIDEBAR} />
</Loader>
</Pane>
</Split>
Expand Down
4 changes: 2 additions & 2 deletions webapp/packages/core-app/src/AppScreen/RightArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Loader, Pane, Placeholder, ResizerControls, s, SlideDialog, Split, useS
import { useService } from '@cloudbeaver/core-di';
import { OptionsPanelService } from '@cloudbeaver/core-ui';

import { AppScreenService } from './AppScreenService.js';
import { AppScreenService, PANEL_ID_MAIN_CONTENT } from './AppScreenService.js';
import style from './RightArea.module.css';

interface Props {
Expand All @@ -33,7 +33,7 @@ export const RightArea = observer<Props>(function RightArea({ className }) {
}

return (
<div className={s(styles, { container: true }, className)}>
<div data-panel-id={PANEL_ID_MAIN_CONTENT} tabIndex={-1} className={s(styles, { container: true }, className)}>
<Split {...splitState} sticky={30} split="horizontal" mode={toolsDisabled ? 'minimize' : splitState.mode} disable={toolsDisabled} keepRatio>
<Pane className={s(styles, { pane: true })}>
<Loader className={s(styles, { loader: true })} suspense>
Expand Down
40 changes: 40 additions & 0 deletions webapp/packages/core-app/src/AppScreen/SkipNavLinks.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@layer components {
.skipNav {
position: absolute;
top: 8px;
left: 8px;
z-index: 10000;
display: flex;
}

.skipNavLink {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
cursor: pointer;
white-space: nowrap;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
padding-inline: var(--dbv-kit-btn-padding-inline);
background: var(--dbv-kit-dialog-content-background);
color: var(--dbv-kit-dialog-content-foreground);
font: inherit;
font-weight: var(--dbv-kit-btn-font-weight);
font-size: var(--dbv-kit-btn-font-size);
border: var(--dbv-kit-btn-border-width) var(--dbv-kit-btn-border-style) var(--dbv-kit-btn-border-color);
border-radius: var(--dbv-kit-btn-border-radius);
box-shadow: var(--dbv-kit-dialog-content-shadow);

&:focus {
position: static;
width: auto;
height: var(--dbv-kit-btn-height);
overflow: visible;
clip: auto;
clip-path: none;
outline: var(--dbv-kit-control-outline-width) solid var(--dbv-kit-control-outline-color);
outline-offset: var(--dbv-kit-control-outline-offset);
}
}
}
37 changes: 37 additions & 0 deletions webapp/packages/core-app/src/AppScreen/SkipNavLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { Placeholder, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { UnstyledButton } from '@dbeaver/ui-kit';

import { PANEL_ID_LEFT_SIDEBAR, PANEL_ID_MAIN_CONTENT } from './AppScreenService.js';
import { SkipNavService } from './SkipNavService.js';
import styles from './SkipNavLinks.module.css';

function focusPanel(panelId: string) {
const element = document.querySelector<HTMLElement>(`[data-panel-id="${panelId}"]`);
element?.focus();
}

export const SkipNavLinks = observer(function SkipNavLinks(): React.ReactElement {
const translate = useTranslate();
const skipNavService = useService(SkipNavService);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should use observable with useTranslate


return (
<nav aria-label={translate('app_skip_nav_label')} className={styles['skipNav']}>
<UnstyledButton type="button" className={styles['skipNavLink']} onClick={() => focusPanel(PANEL_ID_LEFT_SIDEBAR)}>
{translate('app_skip_nav_navigator')}
</UnstyledButton>
<UnstyledButton type="button" className={styles['skipNavLink']} onClick={() => focusPanel(PANEL_ID_MAIN_CONTENT)}>
{translate('app_skip_nav_main_content')}
</UnstyledButton>
<Placeholder container={skipNavService.extraLinks} />
Comment thread
Wroud marked this conversation as resolved.
</nav>
);
});
26 changes: 26 additions & 0 deletions webapp/packages/core-app/src/AppScreen/SkipNavService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { importLazyComponent, PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { injectable } from '@cloudbeaver/core-di';

import { AppScreenService } from './AppScreenService.js';

const SkipNavLinks = importLazyComponent(() => import('./SkipNavLinks.js').then(m => m.SkipNavLinks));

@injectable(() => [AppScreenService])
export class SkipNavService {
readonly extraLinks: PlaceholderContainer;

constructor(private readonly appScreenService: AppScreenService) {
this.extraLinks = new PlaceholderContainer();
}

registerLinks(): void {
this.appScreenService.placeholder.add(SkipNavLinks, 0);
}
}
7 changes: 6 additions & 1 deletion webapp/packages/core-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
Expand All @@ -10,8 +10,13 @@ import './module.js';
// Services
export * from './AppScreen/AppScreenService.js';
export * from './AppScreen/AppScreenBootstrap.js';
export * from './AppScreen/SkipNavService.js';

export * from './AppLocaleService.js';

// components
export * from './BodyLazy.js';

//styles for skip nav links
import styles from './AppScreen/SkipNavLinks.module.css';
export { styles as skipNavStyles };
3 changes: 3 additions & 0 deletions webapp/packages/core-app/src/locales/en.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default [
['app_skip_nav_label', 'Skip navigation'],
['app_skip_nav_navigator', 'Skip to Navigator'],
['app_skip_nav_main_content', 'Skip to Main Content'],
['app_shared_settingsMenu_config', 'Configuration'],
['app_shared_settingsMenu_theme', 'Theme'],
['app_shared_settingsMenu_lang', 'Language'],
Expand Down
3 changes: 3 additions & 0 deletions webapp/packages/core-app/src/locales/fr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default [
['app_skip_nav_label', 'Sauter la navigation'],
['app_skip_nav_navigator', 'Aller au navigateur'],
['app_skip_nav_main_content', 'Aller au contenu principal'],
['app_shared_settingsMenu_config', 'Configuration'],
['app_shared_settingsMenu_theme', 'Thème'],
['app_shared_settingsMenu_lang', 'Langue'],
Expand Down
3 changes: 3 additions & 0 deletions webapp/packages/core-app/src/locales/it.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default [
['app_skip_nav_label', 'Salta la navigazione'],
['app_skip_nav_navigator', 'Vai al navigatore'],
['app_skip_nav_main_content', 'Vai al contenuto principale'],
['app_shared_settingsMenu_config', 'Configurazione'],
['app_shared_settingsMenu_theme', 'Tema'],
['app_shared_settingsMenu_lang', 'Lingua'],
Expand Down
3 changes: 3 additions & 0 deletions webapp/packages/core-app/src/locales/ru.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default [
['app_skip_nav_label', 'Быстрая навигация'],
['app_skip_nav_navigator', 'Перейти к навигатору'],
['app_skip_nav_main_content', 'Перейти к основному содержимому'],
['app_shared_settingsMenu_config', 'Настройки'],
['app_shared_settingsMenu_theme', 'Тема'],
['app_shared_settingsMenu_lang', 'Язык'],
Expand Down
3 changes: 3 additions & 0 deletions webapp/packages/core-app/src/locales/vi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default [
['app_skip_nav_label', 'Bỏ qua điều hướng'],
['app_skip_nav_navigator', 'Chuyển đến trình điều hướng'],
['app_skip_nav_main_content', 'Chuyển đến nội dung chính'],
['app_shared_settingsMenu_config', 'Cấu hình'],
['app_shared_settingsMenu_theme', 'Giao diện'],
['app_shared_settingsMenu_lang', 'Ngôn ngữ'],
Expand Down
3 changes: 3 additions & 0 deletions webapp/packages/core-app/src/locales/zh.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default [
['app_skip_nav_label', '跳过导航'],
['app_skip_nav_navigator', '跳转到导航器'],
['app_skip_nav_main_content', '跳转到主要内容'],
['app_shared_settingsMenu_config', '配置'],
['app_shared_settingsMenu_theme', '主题'],
['app_shared_settingsMenu_lang', '语言'],
Expand Down
9 changes: 7 additions & 2 deletions webapp/packages/core-app/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
Expand All @@ -9,12 +9,17 @@
import { Bootstrap, ModuleRegistry } from '@cloudbeaver/core-di';
import { AppScreenService } from './AppScreen/AppScreenService.js';
import { AppScreenBootstrap } from './AppScreen/AppScreenBootstrap.js';
import { SkipNavService } from './AppScreen/SkipNavService.js';
import { AppLocaleService } from './AppLocaleService.js';

export default ModuleRegistry.add({
name: '@cloudbeaver/core-app',

configure: serviceCollection => {
serviceCollection.addSingleton(Bootstrap, AppLocaleService).addSingleton(Bootstrap, AppScreenBootstrap).addSingleton(AppScreenService);
serviceCollection
.addSingleton(Bootstrap, AppLocaleService)
.addSingleton(Bootstrap, AppScreenBootstrap)
.addSingleton(AppScreenService)
.addSingleton(SkipNavService);
},
});
2 changes: 1 addition & 1 deletion webapp/packages/core-ui/src/SideBarPanel/SideBarPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const SideBarPanel = observer<SideBarPanelProps>(function SideBarPanel({
onChange={tab => selectTab(tab.tabId)}
onReorder={onReorder}
>
<div className={s(style, { box: true })} data-panel-id={panelId} data-dialog-persistent-element>
<div tabIndex={-1} className={s(style, { box: true })} data-panel-id={panelId} data-dialog-persistent-element>
<TabList className={s(style, { tabList: true })} underline />
<div className={s(style, { contentBox: true })}>
<TabPanelList />
Expand Down
1 change: 1 addition & 0 deletions webapp/packages/plugin-help/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@cloudbeaver/plugin-sql-editor": "workspace:*",
"@cloudbeaver/plugin-sql-editor-navigation-tab-script": "workspace:*",
"@cloudbeaver/plugin-top-app-bar": "workspace:*",
"@dbeaver/ui-kit": "workspace:^",
"mobx": "^6",
"mobx-react-lite": "^4",
"react": "^19",
Expand Down
8 changes: 6 additions & 2 deletions webapp/packages/plugin-help/src/PluginBootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { AppScreenService } from '@cloudbeaver/core-app';
import { AppScreenService, SkipNavService } from '@cloudbeaver/core-app';
import { ActionSnackbar, importLazyComponent } from '@cloudbeaver/core-blocks';
import { LocalStorageSaveService } from '@cloudbeaver/core-browser';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
Expand All @@ -18,6 +18,7 @@ import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';

import { ACTION_APP_HELP } from './actions/ACTION_APP_HELP.js';

const SkipNavShortcutsLink = importLazyComponent(() => import('./SkipNavShortcutsLink.js').then(m => m.SkipNavShortcutsLink));
const ShortcutsDialog = importLazyComponent(() => import('./Shortcuts/ShortcutsDialog.js').then(m => m.ShortcutsDialog));
const WelcomeDocs = importLazyComponent(() => import('./WelcomeDocs.js').then(m => m.WelcomeDocs));

Expand All @@ -29,6 +30,7 @@ const WelcomeDocs = importLazyComponent(() => import('./WelcomeDocs.js').then(m
NotificationService,
LocalStorageSaveService,
NavigationTabsService,
SkipNavService,
])
export class PluginBootstrap extends Bootstrap {
private errorNotification: INotification<any> | null;
Expand All @@ -40,6 +42,7 @@ export class PluginBootstrap extends Bootstrap {
private readonly notificationService: NotificationService,
private readonly localStorageSaveService: LocalStorageSaveService,
private readonly navigationTabsService: NavigationTabsService,
private readonly skipNavService: SkipNavService,
) {
super();
this.errorNotification = null;
Expand All @@ -51,6 +54,7 @@ export class PluginBootstrap extends Bootstrap {
this.navigationTabsService.welcomeContainer.add(WelcomeDocs, undefined);
this.addTopAppMenuItems();
this.addMultiTabSupportNotification();
this.skipNavService.extraLinks.add(SkipNavShortcutsLink);
}

private addMultiTabSupportNotification() {
Expand Down
31 changes: 31 additions & 0 deletions webapp/packages/plugin-help/src/SkipNavShortcutsLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { importLazyComponent, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { UnstyledButton } from '@dbeaver/ui-kit';

import { skipNavStyles } from '@cloudbeaver/core-app';

const ShortcutsDialog = importLazyComponent(() => import('./Shortcuts/ShortcutsDialog.js').then(m => m.ShortcutsDialog));

export const SkipNavShortcutsLink = observer(function SkipNavShortcutsLink(): React.ReactElement {
const translate = useTranslate();
const commonDialogService = useService(CommonDialogService);

function handleClick() {
commonDialogService.open(ShortcutsDialog, undefined);
}

return (
<UnstyledButton type="button" className={skipNavStyles['skipNavLink']} onClick={handleClick}>
{translate('shortcuts_title')}
</UnstyledButton>
);
});
Loading
Loading