@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
195 lines (191 loc) • 7.77 kB
JavaScript
'use client';
import { useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base';
import { clsx } from 'clsx';
import { Children, isValidElement, forwardRef, useMemo } from 'react';
import { ObjectPageMode } from '../../enums/ObjectPageMode.js';
import { useObjectPageContext } from '../ObjectPage/context.js';
import { navigateSections } from '../ObjectPage/ObjectPageUtils.js';
import { classNames, styleData } from './ObjectPageSection.module.css.js';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function recursiveSetTabIndexOnSubSection(el, currentTarget) {
if (!el || el === currentTarget) {
return;
}
if (el.dataset.componentName === 'ObjectPageSubSection') {
el.tabIndex = 0;
return;
}
return recursiveSetTabIndexOnSubSection(el.parentElement, currentTarget);
}
/**
* If section has subsections, the first subsection should be next in the tab-chain.
*
* @returns `true` if handled, `false` if not
*/
function setTabIndexForFirstSubSectionIfFocused(e, rootElement, hasSubSection) {
if (e.target === rootElement && hasSubSection) {
const opSubSection = rootElement.querySelector('[data-component-name="ObjectPageSubSection"]');
if (opSubSection) {
opSubSection.tabIndex = 0;
}
return true;
}
return false;
}
/**
* If the target is an interactive element inside a subsection, the subsection should be the next element in the tab-chain.
*
* @returns `true` if handled, `false` if not
*/
function setTabIndexRecursivelyOnSubSections(e, hasSubSection) {
if (hasSubSection) {
recursiveSetTabIndexOnSubSection(e.target, e.currentTarget);
return true;
}
return false;
}
/**
* Top-level information container of an `ObjectPage`.
*/
const ObjectPageSection = /*#__PURE__*/forwardRef((props, ref) => {
const {
titleText,
id,
children,
titleTextUppercase,
className,
style,
hideTitleText,
titleTextLevel = 'H3',
wrapTitleText,
header,
...rest
} = props;
const {
tabRef: _0,
...propsWithoutOmitted
} = rest;
useStylesheet(styleData, ObjectPageSection.displayName);
const [componentRef, sectionRef] = useSyncRef(ref);
const htmlId = `ObjectPageSection-${id}`;
const titleClasses = clsx(classNames.title, titleTextUppercase && classNames.uppercase);
const hasSubSection = useMemo(() => Children.toArray(children).some(
// @ts-expect-error: if type is string, then it's not a subcomponent
child => /*#__PURE__*/isValidElement(child) && child.type?.displayName === 'ObjectPageSubSection'), [children]);
const objectPageMode = useObjectPageContext();
const handleFocusDefault = e => {
if (typeof props.onFocus === 'function') {
props.onFocus(e);
}
// reset tab-index of all sections, so only the focused one is tabbable
Array.from(e.currentTarget.parentElement.children).forEach(el => {
if (el.dataset.componentName === 'ObjectPageSection') {
el.tabIndex = -1;
}
});
e.currentTarget.tabIndex = 0;
if (setTabIndexForFirstSubSectionIfFocused(e, e.currentTarget, hasSubSection)) {
// if section has subsections, the first subsection should be next in the tab-chain
} else if (e.target.dataset.componentName === 'ObjectPageSubSection') {
// if the target is a subsection, the section should be the previous element in the tab-chain
e.target.tabIndex = 0;
} else if (setTabIndexRecursivelyOnSubSections(e, hasSubSection)) {
// If the target is an interactive element inside a subsection, the subsection should be the next element in the tab-chain.
}
};
const handleFocusIconTabBar = e => {
if (typeof props.onFocus === 'function') {
props.onFocus(e);
}
// reference is not updated in time
requestAnimationFrame(() => {
const hasSubSectionDOM = !!sectionRef.current?.querySelector('[data-component-name="ObjectPageSubSection"]');
if (setTabIndexForFirstSubSectionIfFocused(e, sectionRef.current, hasSubSectionDOM)) {
// if section has subsections, the first subsection should be next in the tab-chain
} else if (setTabIndexRecursivelyOnSubSections(e, hasSubSectionDOM)) {
// If the target is an interactive element inside a subsection, the subsection should be the next element in the tab-chain.
}
});
};
const handleBlur = e => {
if (typeof props.onBlur === 'function') {
props.onBlur(e);
}
if (hasSubSection && e.target === e.currentTarget) {
const allSubSections = e.currentTarget.querySelectorAll('[data-component-name="ObjectPageSubSection"]');
allSubSections.forEach(subSection => {
subSection.tabIndex = -1;
});
}
};
const handleKeyDown = e => {
navigateSections({
e,
onKeyDown: props.onKeyDown,
componentName: 'ObjectPageSection'
});
const target = e.currentTarget;
if (target === e.target && (e.key === 'ArrowDown' || e.key === 'ArrowRight') && target.nextElementSibling.dataset.componentName === 'ObjectPageSection') {
e.preventDefault();
// scroll 12px so section is noticed as selected
requestAnimationFrame(() => {
target.parentElement.parentElement.scrollBy(0, 12);
const isFirstSection = target.previousElementSibling.dataset.componentName !== 'ObjectPageSection';
// header collapse leads to loose scrolling - this fallback makes sure the second section is marked as selected
if (isFirstSection) {
setTimeout(() => {
target.parentElement.parentElement.scrollBy(0, 14);
});
}
});
}
if (target === e.target && (e.key === 'ArrowUp' || e.key === 'ArrowLeft') && target.previousElementSibling.dataset.componentName === 'ObjectPageSection') {
e.preventDefault();
// scroll 12px so section is noticed as selected
requestAnimationFrame(() => {
target.parentElement.parentElement.scrollBy(0, 12);
});
}
};
return /*#__PURE__*/_jsxs("section", {
ref: componentRef,
role: "region",
className: clsx(classNames.section, wrapTitleText && classNames.wrap, className),
style: style,
tabIndex: objectPageMode === ObjectPageMode.Default ? -1 : 0,
...propsWithoutOmitted,
id: htmlId,
"data-component-name": "ObjectPageSection",
onFocus: objectPageMode === ObjectPageMode.Default ? handleFocusDefault : handleFocusIconTabBar,
onBlur: objectPageMode === ObjectPageMode.Default ? handleBlur : props.onBlur,
onKeyDown: objectPageMode === ObjectPageMode.Default ? handleKeyDown : props.onKeyDown,
children: [/*#__PURE__*/_jsx("div", {
className: classNames.outlineSpacerDiv,
"aria-hidden": "true"
}), !!header && /*#__PURE__*/_jsx("div", {
className: clsx(classNames.headerContainer, !hasSubSection ? classNames.sticky : undefined),
children: header
}), !hideTitleText && /*#__PURE__*/_jsx("div", {
role: "heading",
"aria-level": parseInt(titleTextLevel.slice(1)),
className: clsx(classNames.titleContainer, !header && !hasSubSection ? classNames.sticky : undefined),
"data-component-name": "ObjectPageSectionTitleText",
children: /*#__PURE__*/_jsx("div", {
className: titleClasses,
children: titleText
})
}), /*#__PURE__*/_jsx("div", {
className: classNames.sectionContent,
children: /*#__PURE__*/_jsx("div", {
className: classNames.sectionContentInner,
"data-component-name": "ObjectPageSectionContent",
children: children
})
}), /*#__PURE__*/_jsx("div", {
className: classNames.outlineSpacerDiv,
"aria-hidden": "true"
})]
});
});
ObjectPageSection.displayName = 'ObjectPageSection';
export { ObjectPageSection };