Skip to content

Commit 9f5c341

Browse files
committed
handle tabindex when subsection or interactive element inside (sub)section is focused
1 parent a39b56b commit 9f5c341

File tree

3 files changed

+46
-12
lines changed

3 files changed

+46
-12
lines changed

packages/main/src/components/ObjectPage/ObjectPageUtils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,23 @@ export function navigateSections({ e, onKeyDown, componentName }: NavigateSectio
3434

3535
const nextSibling = e.currentTarget.nextElementSibling as HTMLElement;
3636
const prevSibling = e.currentTarget.previousElementSibling as HTMLElement;
37-
if ((e.key === 'ArrowDown' || e.key === 'ArrowRight') && nextSibling.dataset.componentName === componentName) {
37+
if (
38+
nextSibling &&
39+
(e.key === 'ArrowDown' || e.key === 'ArrowRight') &&
40+
nextSibling.dataset.componentName === componentName
41+
) {
3842
e.preventDefault();
3943
e.currentTarget.tabIndex = -1;
4044
nextSibling.tabIndex = 0;
4145
nextSibling.focus({ preventScroll: true });
4246
nextSibling.scrollIntoView({ behavior: 'instant', block: 'start' });
4347
}
4448

45-
if ((e.key === 'ArrowUp' || e.key === 'ArrowLeft') && prevSibling.dataset.componentName === componentName) {
49+
if (
50+
prevSibling &&
51+
(e.key === 'ArrowUp' || e.key === 'ArrowLeft') &&
52+
prevSibling.dataset.componentName === componentName
53+
) {
4654
e.preventDefault();
4755
e.currentTarget.tabIndex = -1;
4856
prevSibling.tabIndex = 0;

packages/main/src/components/ObjectPageSection/index.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type TitleLevel from '@ui5/webcomponents/dist/types/TitleLevel.js';
44
import { useStylesheet } from '@ui5/webcomponents-react-base';
55
import { clsx } from 'clsx';
66
import type { ReactNode, FocusEventHandler, KeyboardEventHandler } from 'react';
7-
import { Children, isValidElement, forwardRef } from 'react';
7+
import { Children, isValidElement, forwardRef, useMemo } from 'react';
88
import type { CommonProps } from '../../types/index.js';
99
import { navigateSections } from '../ObjectPage/ObjectPageUtils.js';
1010
import { classNames, styleData } from './ObjectPageSection.module.css.js';
@@ -57,6 +57,18 @@ export interface ObjectPageSectionPropTypes extends CommonProps {
5757
header?: ReactNode;
5858
}
5959

60+
function recursiveSetTabIndexOnSubSection(el: HTMLElement | null, currentTarget: HTMLElement): void {
61+
if (!el || el === currentTarget) {
62+
return;
63+
}
64+
65+
if (el.dataset.componentName === 'ObjectPageSubSection') {
66+
el.tabIndex = 0;
67+
return;
68+
}
69+
return recursiveSetTabIndexOnSubSection(el.parentElement, currentTarget);
70+
}
71+
6072
/**
6173
* Top-level information container of an `ObjectPage`.
6274
*/
@@ -77,6 +89,14 @@ const ObjectPageSection = forwardRef<HTMLElement, ObjectPageSectionPropTypes>((p
7789
useStylesheet(styleData, ObjectPageSection.displayName);
7890
const htmlId = `ObjectPageSection-${id}`;
7991
const titleClasses = clsx(classNames.title, titleTextUppercase && classNames.uppercase);
92+
const hasSubSection = useMemo(
93+
() =>
94+
Children.toArray(children).some(
95+
// @ts-expect-error: if type is string, then it's not a subcomponent
96+
(child) => isValidElement(child) && child.type?.displayName === 'ObjectPageSubSection',
97+
),
98+
[children],
99+
);
80100

81101
const handleFocus: FocusEventHandler<HTMLElement> = (e) => {
82102
if (typeof props.onFocus === 'function') {
@@ -88,27 +108,28 @@ const ObjectPageSection = forwardRef<HTMLElement, ObjectPageSectionPropTypes>((p
88108
el.tabIndex = -1;
89109
}
90110
});
111+
91112
e.currentTarget.tabIndex = 0;
92-
const hasSubSection = Children.toArray(children).some(
93-
// @ts-expect-error: if type is string, then it's not a subcomponent
94-
(child) => isValidElement(child) && child.type?.displayName === 'ObjectPageSubSection',
95-
);
96-
if (hasSubSection && e.target === e.currentTarget) {
113+
// if section has subsections, the first subsection should be next in the tab-chain
114+
if (e.target === e.currentTarget && hasSubSection) {
97115
const opSubSection: HTMLElement = e.currentTarget.querySelector('[data-component-name="ObjectPageSubSection"]');
98116
if (opSubSection) {
99117
opSubSection.tabIndex = 0;
100118
}
119+
// if the target is a subsection, the section should be the previous element in the tab-chain
120+
} else if (e.target.dataset.componentName === 'ObjectPageSubSection') {
121+
e.target.tabIndex = 0;
122+
// if the target is an interactive element inside a subsection, the subsection should be the next element in the tab-chain
123+
} else if (hasSubSection) {
124+
recursiveSetTabIndexOnSubSection(e.target, e.currentTarget);
101125
}
102126
};
103127

104128
const handleBlur: FocusEventHandler<HTMLElement> = (e) => {
105129
if (typeof props.onBlur === 'function') {
106130
props.onBlur(e);
107131
}
108-
const hasSubSection = Children.toArray(children).some(
109-
// @ts-expect-error: if type is string, then it's not a subcomponent
110-
(child) => isValidElement(child) && child.type?.displayName === 'ObjectPageSubSection',
111-
);
132+
112133
if (hasSubSection && e.target === e.currentTarget) {
113134
const allSubSections: NodeListOf<HTMLElement> = e.currentTarget.querySelectorAll(
114135
'[data-component-name="ObjectPageSubSection"]',

packages/main/src/components/ObjectPageSubSection/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ const ObjectPageSubSection = forwardRef<HTMLDivElement, ObjectPageSubSectionProp
8787
onKeyDown={(e) => {
8888
navigateSections({ e, onKeyDown: props.onKeyDown, componentName: 'ObjectPageSubSection' });
8989
}}
90+
onBlur={(e) => {
91+
if (e.target === e.currentTarget) {
92+
e.target.tabIndex = -1;
93+
}
94+
}}
9095
className={subSectionClassName}
9196
id={htmlId}
9297
data-component-name="ObjectPageSubSection"

0 commit comments

Comments
 (0)