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

CB-5446 chore: keyboard navigation improvements #2935

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion webapp/packages/core-blocks/src/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const IconButton: React.FC<IconButtonProps> = observer(function IconButto
const Button = tag ?? ReakitButton;

return (
<Button {...rest} className={s(styles, { iconButton: true }, className)}>
<Button tabIndex={0} {...rest} className={s(styles, { iconButton: true }, className)}>
<div className={s(styles, { iconBox: true })}>
{img && <StaticImage className={s(styles, { staticImage: true })} icon={name} />}
{!img && <Icon className={s(styles, { icon: true })} name={name} viewBox={viewBox} />}
Expand Down
2 changes: 2 additions & 0 deletions webapp/packages/core-blocks/src/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(
<MenuButton
key={relativePosition ? 'link' : 'main'}
ref={combinedRef}
tabIndex={0}
className={s(styles, { menuButton: true }, className)}
{...menu}
visible={menuVisible}
Expand Down Expand Up @@ -168,6 +169,7 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(
<MenuButton
key={relativePosition ? 'link' : 'main'}
ref={combinedRef}
tabIndex={0}
className={s(styles, { menuButton: true }, className)}
{...menu}
visible={menuVisible}
Expand Down
7 changes: 4 additions & 3 deletions webapp/packages/core-blocks/src/TextPlaceholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import type { HTMLAttributes } from 'react';

import { s } from './s.js';
import style from './TextPlaceholder.module.css';
import { useS } from './useS.js';

interface Props {
interface Props extends HTMLAttributes<HTMLDivElement> {
className?: string;
children?: React.ReactNode;
}

export const TextPlaceholder = observer<Props>(function TextPlaceholder({ className, children }) {
export const TextPlaceholder = observer<Props>(function TextPlaceholder({ className, children, ...rest }) {
const styles = useS(style);

return (
<div className={s(styles, { container: true })}>
<div {...rest} className={s(styles, { container: true })}>
<span className={s(styles, { content: true }, className)}>{children}</span>
</div>
);
Expand Down
10 changes: 8 additions & 2 deletions webapp/packages/core-blocks/src/ToolsPanel/ToolsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import type { HTMLAttributes } from 'react';

import { s } from '../s.js';
import { useS } from '../useS.js';
import style from './ToolsPanel.module.css';

type TType = 'primary' | 'secondary';
interface Props {
interface Props extends HTMLAttributes<HTMLDivElement> {
className?: string;
type?: TType;
center?: boolean;
Expand All @@ -29,8 +30,13 @@ export const ToolsPanel: React.FC<React.PropsWithChildren<Props>> = observer(fun
minHeight,
type = 'primary',
bottomBorder = false,
...rest
}) {
const styles = useS(style);

return <div className={s(styles, { toolsPanel: true, [type]: true, bottomBorder, minHeight, center, rounded }, className)}>{children}</div>;
return (
<div {...rest} className={s(styles, { toolsPanel: true, [type]: true, bottomBorder, minHeight, center, rounded }, className)}>
{children}
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ export const TreeNodeControl = observer<Props & React.HTMLAttributes<HTMLDivElem
return (
<div
ref={ref}
tabIndex={0}
tabIndex={context.selected ? 0 : -1}
title={title}
aria-selected={context.selected}
className={s(styles, { treeNodeControl: true }, className)}
data-tree-node-control
onClick={handleClick}
onMouseDown={handleMouseDown}
onKeyDown={handleEnter}
Expand Down
1 change: 1 addition & 0 deletions webapp/packages/core-blocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export * from './Snackbars/Snackbar.js';
export * from './Snackbars/ActionSnackbar.js';
export * from './Snackbars/ProcessSnackbar.js';
export * from './useUserData.js';
export * from './useListKeyboardNavigation.js';
export * from './useMergeRefs.js';
export * from './usePasswordValidation.js';
export * from './manifest.js';
Expand Down
100 changes: 100 additions & 0 deletions webapp/packages/core-blocks/src/useListKeyboardNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 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 { useEffect, useState } from 'react';

import { EventContext } from '@cloudbeaver/core-events';

export const EventKeyboardNavigationFlag = EventContext.create('useListKeyboardNavigation');

export function useListKeyboardNavigation<T extends HTMLElement>(elementsSelector = '[tabindex]:not(:disabled)'): (obj: T | null) => void {
const [ref, setRef] = useState<T | null>(null);

useEffect(() => {
if (!ref) {
return;
}

const getFocusableElements = () => {
const allFocusableElements = Array.from(ref.querySelectorAll(elementsSelector)) as HTMLElement[];
return allFocusableElements;
};

// Function to reset tabindex on all elements and set it to 0 on aria-selected="true"
const resetTabindex = () => {
const focusableElements = getFocusableElements();
focusableElements.forEach(el => {
if (el.getAttribute('aria-selected') === 'true') {
el.setAttribute('tabindex', '0');
} else {
el.setAttribute('tabindex', '-1');
}
Comment on lines +33 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose this set of elements can include elements without aria-selected attribute. Is this okay to set tabIndex for them also to -1? Or it is supposed to be only for elements with aria-selected attribute?

Copy link
Member Author

Choose a reason for hiding this comment

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

this is okay, if element don't have this attribute then tabindex=-1 is okay, but here more problems over -1 and 0 because sometimes you can't restore initial value (when you don't have aria-selected or when element was tabindex=0 initially)

but for now seems that it works, maybe we will found bugs related to this function later

});
};

const handleKeyDown = (e: KeyboardEvent) => {
if (EventContext.has(e, EventKeyboardNavigationFlag) || !['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
return;
}

const focusableElements = getFocusableElements();
let currentIndex = focusableElements.findIndex(el => el === document.activeElement);

if (document.activeElement !== ref && currentIndex === -1) {
return;
}

let newIndex = currentIndex;

EventContext.set(e, EventKeyboardNavigationFlag);
e.preventDefault();

switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
newIndex = (currentIndex + 1) % focusableElements.length; // Move to next element
Copy link
Contributor

Choose a reason for hiding this comment

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

lets move to next/prev element into functions


break;
case 'ArrowLeft':
case 'ArrowUp':
if (currentIndex === -1) {
currentIndex = 0;
}
newIndex = (currentIndex - 1 + focusableElements.length) % focusableElements.length; // Move to previous element

break;
default:
return;
}

// // Reset all tabindex to -1
focusableElements.forEach(el => el.setAttribute('tabindex', '-1'));

// Set the new element's tabindex to 0 and focus it
const nextElement = focusableElements[newIndex];
nextElement?.setAttribute('tabindex', '0');
nextElement?.focus();
};

const handleFocusOut = (e: FocusEvent) => {
// Check if the focus moved outside the container
if (!ref.contains(e.relatedTarget as Node)) {
resetTabindex();
}
};

ref.addEventListener('keydown', handleKeyDown);
ref.addEventListener('focusout', handleFocusOut);

return () => {
ref.removeEventListener('keydown', handleKeyDown);
ref.removeEventListener('focusout', handleFocusOut);
};
}, [ref, elementsSelector]);

return setRef;
}
45 changes: 30 additions & 15 deletions webapp/packages/core-theming/src/styles/main/normalize.pure.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */

/* Document
Expand Down Expand Up @@ -216,9 +223,9 @@ select {
*/

button,
[type="button"],
[type="reset"],
[type="submit"] {
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}

Expand All @@ -227,9 +234,9 @@ button,
*/

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
[type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
Expand All @@ -239,9 +246,9 @@ button::-moz-focus-inner,
*/

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
[type='button']:-moz-focusring,
[type='reset']:-moz-focusring,
[type='submit']:-moz-focusring {
outline: 1px dotted ButtonText;
}

Expand Down Expand Up @@ -296,8 +303,8 @@ textarea {
* 2. Remove the padding in IE 10.
*/

[type="checkbox"],
[type="radio"] {
[type='checkbox'],
[type='radio'] {
box-sizing: border-box;
/* 1 */
padding: 0;
Expand All @@ -308,8 +315,8 @@ textarea {
* Correct the cursor style of increment and decrement buttons in Chrome.
*/

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
[type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button {
height: auto;
}

Expand All @@ -318,18 +325,26 @@ textarea {
* 2. Correct the outline style in Safari.
*/

[type="search"] {
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}

/**
* outline styles
*/

:focus-visible {
outline-offset: -1px;
}

/**
* Remove the inner padding in Chrome and Safari on macOS.
*/

[type="search"]::-webkit-search-decoration {
[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}

Expand Down
6 changes: 5 additions & 1 deletion webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
SContext,
type StyleRegistry,
useAutoLoad,
useListKeyboardNavigation,
useMergeRefs,
useS,
} from '@cloudbeaver/core-blocks';
import { type IDataContext, useDataContextLink } from '@cloudbeaver/core-data-context';
Expand Down Expand Up @@ -52,6 +54,8 @@ const styleRegistry: StyleRegistry = [

export const MenuBar = observer<IMenuBarProps, HTMLDivElement>(
forwardRef(function MenuBar({ menu, nestedMenuSettings, rtl, className, ...props }, ref) {
const refNav = useListKeyboardNavigation();
const mergedRef = useMergeRefs(ref, refNav);
const styles = useS(style);
const items = menu.items;
useAutoLoad(MenuBar, menu.loaders);
Expand All @@ -62,7 +66,7 @@ export const MenuBar = observer<IMenuBarProps, HTMLDivElement>(

return (
<SContext registry={styleRegistry}>
<div ref={ref} className={s(styles, { menuBar: true }, className)} {...props}>
<div ref={mergedRef} className={s(styles, { menuBar: true }, className)} tabIndex={0} {...props}>
<Loader suspense small>
{items.map(item => (
<MenuBarElement key={item.id} item={item} menuData={menu} nestedMenuSettings={nestedMenuSettings} rtl={rtl} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ export const MenuBarItem = registry(

const title = translate(rest.title);
const Submenu = submenu;
const selected = rest['aria-selected'] === 'true';
const disabled = rest.disabled;
const tabIndex = selected ? 0 : -1;

return (
<div
className={s(styles, { menuBarItemGroup: true, hidden, disabled: rest.disabled }, className)}
className={s(styles, { menuBarItemGroup: true, hidden, disabled }, className)}
aria-selected={rest['aria-selected']}
aria-disabled={rest['aria-disabled']}
>
<button ref={ref} className={s(styles, { menuBarItem: true })} {...rest} title={title} aria-label={title}>
<button ref={ref} className={s(styles, { menuBarItem: true })} tabIndex={tabIndex} {...rest} title={title} aria-label={title}>
<div className={s(styles, { menuBarItemBox: true })}>
{loading ? (
<div className={s(styles, { menuBarItemIcon: true })}>
Expand Down
3 changes: 1 addition & 2 deletions webapp/packages/core-ui/src/Tabs/Tab/Tab.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

.tab {
position: relative;
outline: none;
font-weight: normal;
cursor: pointer;
padding: 0;
Expand All @@ -28,7 +27,7 @@
border-top-color: var(--theme-negative);
opacity: 1;

&:before {
&:not(:focus-visible):before {
display: none;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
color: inherit;
border: none;
border-bottom: 2px solid var(--theme-primary);
outline: none;
opacity: 1;
border-top: none;
font-weight: normal;
Expand Down
1 change: 0 additions & 1 deletion webapp/packages/core-ui/src/Tabs/TabList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
.tabList {
display: flex;
box-sizing: border-box;
outline: none;
}

.underline {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@
flex: 0 0 auto;
min-width: 150px;
overflow: auto;
outline: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
.vertical.rotated.tabList {
display: flex;
gap: 4px;
outline: none;
max-width: 32px;
overflow: hidden;
flex-direction: column;
Expand Down
Loading