Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(editor): fix accessibility issues with editor tabs and close buttons #3916

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
94 changes: 92 additions & 2 deletions packages/editor/src/browser/tab.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { LayoutViewSizeConfig } from '@opensumi/ide-core-browser/lib/layout/cons
import { VIEW_CONTAINERS } from '@opensumi/ide-core-browser/lib/layout/view-id';
import { IMenuRegistry, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
import { useInjectable, useUpdateOnEventBusEvent } from '@opensumi/ide-core-browser/lib/react-hooks';
import { formatLocalize, isMacintosh } from '@opensumi/ide-core-common';

import {
IEditorGroup,
Expand Down Expand Up @@ -370,15 +371,91 @@ export const Tabs = ({ group }: ITabsProps) => {
[editorService],
);

// 处理选项卡键盘事件
const handleKeyDown = (e: React.KeyboardEvent) => {
Copy link
Member

Choose a reason for hiding this comment

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

在 OpenSumi 中目前针对快捷键有统一实现,不建议通过这种方式绑定键盘事件处理,可以通过 keybindingService 去注册快捷键方式实现,而选项卡是否聚焦,应该设定相应的 when 来控制,相关可参考的代码见:

registerKeybindings(keybindings: KeybindingRegistry) {

注册与前端组件绑定的 contextKey(when):

initContextKey(dom: HTMLDivElement) {

当前实现后续可能会出现快捷键冲突问题

Copy link
Contributor Author

Choose a reason for hiding this comment

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

收到
请问a给组件添加aria、role、tabIndex属性这些可以直接加在组件上吗,有没有统一处理机制。我们先加常规的无障碍属性和tabIndex。键盘的单独提交

if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', ' '].includes(e.key)) {
e.stopPropagation();
e.preventDefault();
}
if ([' ', 'Enter'].includes(e.key)) {
simulateClick(e.currentTarget as HTMLElement);
} else if (e.key === 'ContextMenu') {
simulateContextMenu(e.currentTarget as HTMLElement);
}

handleTabNavigation(e);
};

const simulateClick = (element: HTMLElement) => {
const mouseDownEvent = new window.MouseEvent('mousedown', { bubbles: true, button: 0 });
Copy link
Member

Choose a reason for hiding this comment

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

同理,可以和 onMousedown 事件的处理方式一样

Copy link
Contributor Author

Choose a reason for hiding this comment

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

收到,谢谢

element.dispatchEvent(mouseDownEvent);
};

const simulateContextMenu = (element: HTMLElement) => {
const mouseDownEvent = new window.MouseEvent('contextmenu', { bubbles: true, button: 2 });
Copy link
Member

Choose a reason for hiding this comment

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

这里的 simulateContextMenu 可以直接调用 tabTitleMenuService.show 方法,和 onContextMenu 的事件处理一致就行

element.dispatchEvent(mouseDownEvent);
};

const handleTabNavigation = (e: React.KeyboardEvent) => {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) {
navigateTabs(e);
}
};

const navigateTabs = (e: React.KeyboardEvent) => {
const currentElement = e.currentTarget;
const parentNode = currentElement.parentElement?.parentElement?.parentElement;
if (parentNode) {
const tabs = Array.from(parentNode.querySelectorAll('[role="tab"][aria-expanded]')) as HTMLElement[];
Copy link
Member

Choose a reason for hiding this comment

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

这里通过 react 的 ref 去访问 role='tab' 的元素吧,不要通过 querySelectorAll 的方式。

if (tabs.length <= 1) {
return;
}

const currentTabIndex = tabs.findIndex((tab) => tab === currentElement);
if (currentTabIndex === -1) {
return;
}

const moveFocus = (targetIndex: number) => {
if (targetIndex >= 0 && targetIndex < tabs.length) {
const targetTab = tabs[targetIndex];
if (targetTab) {
targetTab.setAttribute('tabindex', '0');
targetTab.focus();
currentElement.setAttribute('tabindex', '-1');
}
}
};

if (['ArrowLeft', 'ArrowUp'].includes(e.key)) {
Copy link
Member

Choose a reason for hiding this comment

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

这里的硬编码都使用 packages/core-browser/src/keyboard/keys.ts 文件里的变量吧

moveFocus(currentTabIndex - 1);
} else if (['ArrowRight', 'ArrowDown'].includes(e.key)) {
moveFocus(currentTabIndex + 1);
} else if (e.key === 'Home') {
moveFocus(0);
} else if (e.key === 'End') {
moveFocus(tabs.length - 1);
}
}
};

const renderEditorTab = React.useCallback(
(resource: IResource, isCurrent: boolean) => {
const decoration = resourceService.getResourceDecoration(resource.uri);
const subname = resourceService.getResourceSubname(resource, group.resources);
const editorCloseTabButtonAriaLabel = formatLocalize('editor.closeTab.title', resource.name);

return editorTabService.renderEditorTab(
<>
<div className={tabsLoadingMap[resource.uri.toString()] ? 'loading_indicator' : cls(resource.icon)}> </div>
<div>{resource.name}</div>
<div
role='tab'
tabIndex={isCurrent ? 0 : -1}
aria-expanded={isCurrent ? 'true' : 'false'}
onKeyDown={handleKeyDown}
>
{resource.name}
</div>
{subname ? <div className={styles.subname}>{subname}</div> : null}
{decoration.readOnly ? (
<span className={cls(getExternalIcon('lock'), styles.editor_readonly_icon)}></span>
Expand All @@ -396,6 +473,19 @@ export const Tabs = ({ group }: ITabsProps) => {
e.stopPropagation();
group.close(resource.uri);
}}
tabIndex={isCurrent ? 0 : -1}
role='button'
aria-label={editorCloseTabButtonAriaLabel}
aria-description={isMacintosh ? '⌘W' : 'Ctrl+W'}
Copy link
Member

Choose a reason for hiding this comment

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

这里的快捷键 ⌘W 是跟随着快捷键配置走的,不一定一直是 ⌘W。
你可以通过 keybindingRegistry 来获取 EDITOR_COMMANDS.CLOSE.id 所绑定的快捷键信息,并将其转为文案显示。可以参考这里
https://github.com/opensumi/core/blob/v3.2/packages/ai-native/src/browser/widget/inline-hint/inline-hint-line-widget.tsx#L38-L45

onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
// 模拟鼠标左键点击事件
const mouseDownEvent = new window.MouseEvent('mousedown', { bubbles: true, button: 0 });
Copy link
Member

Choose a reason for hiding this comment

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

直接调用 group.close(resource.uri) 吧。和 onMouseDown 的事件一样,不需要通过 MouseEvent 去模拟

e.currentTarget.dispatchEvent(mouseDownEvent);
}
}}
>
{editorTabService.renderTabCloseComponent(
<div className={cls(getIcon('close'), styles_kt_editor_close_icon)} />,
Expand Down Expand Up @@ -500,7 +590,7 @@ export const Tabs = ({ group }: ITabsProps) => {
};

return (
<div id={VIEW_CONTAINERS.EDITOR_TABS} className={styles_kt_editor_tabs}>
<div id={VIEW_CONTAINERS.EDITOR_TABS} className={styles_kt_editor_tabs} role='tablist'>
<div
className={styles_kt_editor_tabs_scroll_wrapper}
ref={tabWrapperRef as any}
Expand Down
Loading