diff --git a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx index c506d7f89a2..dcc15a9166d 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.cy.tsx @@ -43,9 +43,14 @@ import { Title, Toolbar, ToolbarButton, + Table, + TableCell, + TableHeaderCell, + TableHeaderRow, + TableRow, } from '../..'; -import { cypressPassThroughTestsFactory } from '@/cypress/support/utils'; import type { TabDomRef } from '../../webComponents/Tab/index.js'; +import { cypressPassThroughTestsFactory } from '@/cypress/support/utils'; const arbitraryCharsId = `~\`!1@#$%^&*()-_+={}[]:;"'z,<.>/?|♥`; @@ -1651,6 +1656,22 @@ describe('ObjectPage', () => { + + + + Product + Supplier + Price + + {new Array(20).fill(1337).map((_, i) => ( + + Mac + Apple + 10.09 + + ))} +
+
, ); @@ -1691,9 +1712,18 @@ describe('ObjectPage', () => { // 6.2 input cy.realPress('Tab'); cy.findByTestId('sub').should('be.focused'); + // Table + cy.realPress('Tab'); + cy.focused().should('have.attr', 'ui5-table-row'); //footer cy.realPress('Tab'); cy.findByTestId('footer-accept-btn').should('be.focused'); + // Table + cy.realPress(['Shift', 'Tab']); + cy.focused().should('have.attr', 'ui5-table-row'); + // Table Section + cy.realPress(['Shift', 'Tab']); + cy.focused().should('have.attr', 'aria-label', 'Table').and('have.attr', 'tabindex', 0); // 6.2 input cy.realPress(['Shift', 'Tab']); cy.findByTestId('sub').should('be.focused'); @@ -1730,10 +1760,12 @@ describe('ObjectPage', () => { cy.get('[data-component-name="ObjectPageSubSection"]').should('have.attr', 'tabindex', -1); // click first Tab - cy.focused().realClick(); + cy.log('click first Tab'); + cy.get('[ui5-tabcontainer]').findUi5TabByText('Goals').click(); cy.focused().should('have.attr', 'aria-label', 'Goals').and('have.attr', 'tabindex', 0); // arrow section navigation + cy.log('arrow section navigation'); cy.realPress('ArrowUp'); cy.focused().should('have.attr', 'aria-label', 'Goals').and('have.attr', 'tabindex', 0); cy.realPress('ArrowDown'); @@ -1747,9 +1779,25 @@ describe('ObjectPage', () => { cy.realPress('ArrowDown'); cy.focused().should('have.attr', 'aria-label', 'SubSectionsInput').and('have.attr', 'tabindex', 0); cy.realPress('ArrowDown'); - cy.focused().should('have.attr', 'aria-label', 'SubSectionsInput').and('have.attr', 'tabindex', 0); + cy.focused() + .should('have.attr', 'aria-label', 'Table') + .and('have.attr', 'tabindex', 0) + .then(($el) => { + const rect = $el[0].getBoundingClientRect(); + expect(rect.top).to.be.at.most(214); + }); + cy.realPress('ArrowDown'); + cy.focused() + .should('have.attr', 'aria-label', 'Table') + .and('have.attr', 'tabindex', 0) + .then(($el) => { + const rect = $el[0].getBoundingClientRect(); + expect(rect.top).to.be.at.most(211); + }); // arrow subsection navigation + cy.log('arrow subsection navigation'); + cy.realPress('ArrowUp'); cy.realPress('Tab'); cy.focused().should('have.attr', 'aria-label', '6.1').and('have.attr', 'tabindex', 0); cy.realPress('ArrowUp'); @@ -1777,6 +1825,19 @@ describe('ObjectPage', () => { cy.wrap(section).should('have.attr', 'tabindex', -1); } }); + + //Table row navigation (relatedTarget not present - scroll-padding fallback) + cy.log('Table row navigation'); + cy.get('[ui5-tabcontainer]').findUi5TabByText('Table').click(); + cy.realPress('Tab'); + for (let i = 0; i < 15; i++) { + cy.realPress('ArrowDown'); + } + cy.focused().should('be.visible').and('have.attr', 'ui5-table-row'); + for (let i = 0; i < 13; i++) { + cy.realPress('ArrowUp'); + } + cy.focused().should('be.visible').and('have.attr', 'ui5-table-row'); }); }); diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index e1ac91c4bc1..8877eac50fb 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -10,7 +10,7 @@ import { useSyncRef, } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; -import type { CSSProperties, MouseEventHandler, ReactElement, UIEventHandler } from 'react'; +import type { CSSProperties, FocusEventHandler, MouseEventHandler, ReactElement, UIEventHandler } from 'react'; import { cloneElement, forwardRef, isValidElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ObjectPageMode } from '../../enums/ObjectPageMode.js'; import { safeGetChildrenArray } from '../../internal/safeGetChildrenArray.js'; @@ -149,6 +149,7 @@ const ObjectPage = forwardRef((props, ref scrollTimeout, }, ); + const scrollPaddingBlock = `${Math.ceil(12 + topHeaderHeight + TAB_CONTAINER_HEADER_HEIGHT + (!headerCollapsed && headerPinned ? headerContentHeight : 0))}px ${footerArea ? 'calc(var(--_ui5wcr-BarHeight) + 1.25rem)' : 0}`; useEffect(() => { if (typeof onToggleHeaderArea === 'function' && isToggledRef.current) { @@ -625,6 +626,25 @@ const ObjectPage = forwardRef((props, ref } }, [isMounted, children, mode]); + const handleContentBlur: FocusEventHandler = (e) => { + const opNode = objectPageRef.current; + if (!opNode) return; + + if (e.relatedTarget && !e.currentTarget.contains(e.relatedTarget as Node)) { + opNode.style.scrollPaddingBlock = '0px'; + // Fallback: Some (ui5-table) ui5wc components don't implement `relatedTarget` as expected. + } else if (!e.relatedTarget) { + const currentTarget = e.currentTarget; + opNode.style.scrollPaddingBlock = '0px'; + requestAnimationFrame(() => { + if (currentTarget.contains(document.activeElement)) { + opNode.style.scrollPaddingBlock = scrollPaddingBlock; + document.activeElement.scrollIntoView({ block: 'nearest' }); + } + }); + } + }; + return (
((props, ref const opNode = objectPageRef.current; if (opNode) { // 12px or 0.75rem margin for ui5wc border and input margins - opNode.style.scrollPaddingBlock = `${Math.ceil(12 + topHeaderHeight + TAB_CONTAINER_HEADER_HEIGHT + (!headerCollapsed && headerPinned ? headerContentHeight : 0))}px ${footerArea ? 'calc(var(--_ui5wcr-BarHeight) + 1.25rem)' : 0}`; - } - }} - onBlur={(e) => { - const opNode = objectPageRef.current; - if (opNode && !e.currentTarget.contains(e.relatedTarget as Node)) { - opNode.style.scrollPaddingBlock = '0px'; + opNode.style.scrollPaddingBlock = scrollPaddingBlock; } }} + onBlur={handleContentBlur} >
((p navigateSections({ e, onKeyDown: props.onKeyDown, componentName: 'ObjectPageSection' }); const target = e.currentTarget as HTMLElement; if ( + target === e.target && (e.key === 'ArrowDown' || e.key === 'ArrowRight') && (target.nextElementSibling as HTMLElement).dataset.componentName === 'ObjectPageSection' ) { @@ -219,6 +220,7 @@ const ObjectPageSection = forwardRef((p }); } if ( + target === e.target && (e.key === 'ArrowUp' || e.key === 'ArrowLeft') && (target.previousElementSibling as HTMLElement).dataset.componentName === 'ObjectPageSection' ) {