Skip to content

Commit

Permalink
CB-5446 chore: keyboard navigation improvements (#2935)
Browse files Browse the repository at this point in the history
* chore: keyboard navigation improvements

* CB-5446 chore: use theme color for outline

---------

Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com>
  • Loading branch information
Wroud and EvgeniaBzzz authored Sep 26, 2024
1 parent 0e3da49 commit 0bf6783
Show file tree
Hide file tree
Showing 35 changed files with 227 additions and 62 deletions.
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');
}
});
};

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

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

0 comments on commit 0bf6783

Please sign in to comment.