UNPKG

@ui5/webcomponents-react

Version:

React Wrapper for UI5 Web Components and additional components

195 lines (191 loc) 7.77 kB
'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 };