-
Notifications
You must be signed in to change notification settings - Fork 396
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -370,15 +371,91 @@ export const Tabs = ({ group }: ITabsProps) => { | |
[editorService], | ||
); | ||
|
||
// 处理选项卡键盘事件 | ||
const handleKeyDown = (e: React.KeyboardEvent) => { | ||
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 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 同理,可以和 onMousedown 事件的处理方式一样 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
@@ -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'} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里的快捷键 ⌘W 是跟随着快捷键配置走的,不一定一直是 ⌘W。 |
||
onKeyDown={(e) => { | ||
if (e.key === ' ' || e.key === 'Enter') { | ||
e.stopPropagation(); | ||
e.preventDefault(); | ||
// 模拟鼠标左键点击事件 | ||
const mouseDownEvent = new window.MouseEvent('mousedown', { bubbles: true, button: 0 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)} />, | ||
|
@@ -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} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在 OpenSumi 中目前针对快捷键有统一实现,不建议通过这种方式绑定键盘事件处理,可以通过 keybindingService 去注册快捷键方式实现,而选项卡是否聚焦,应该设定相应的 when 来控制,相关可参考的代码见:
core/packages/debug/src/browser/debug-contribution.ts
Line 681 in ec0fc77
注册与前端组件绑定的 contextKey(when):
core/packages/terminal-next/src/browser/terminal.controller.ts
Line 409 in d26c0ef
当前实现后续可能会出现快捷键冲突问题
There was a problem hiding this comment.
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。键盘的单独提交