From 94f52d2910e43d31d2822f4b50c0126deb654952 Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 22 Oct 2026 15:26:43 +0200 Subject: [PATCH 1/2] feat(ObjectPage): derive section accessible names from titles --- .../ObjectPage/ObjectPage.stories.tsx | 68 ++++--------------- .../ObjectPage/test/ObjectPage.gallery.tsx | 24 +++++++ .../ObjectPage/test/ObjectPage.spec.tsx | 19 ++++++ .../components/ObjectPageSection/index.tsx | 6 ++ .../components/ObjectPageSubSection/index.tsx | 6 ++ 5 files changed, 68 insertions(+), 55 deletions(-) diff --git a/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx b/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx index 7ec697c637b..b501c31d53e 100644 --- a/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx +++ b/packages/main/src/components/ObjectPage/ObjectPage.stories.tsx @@ -167,7 +167,7 @@ export const Default: Story = { return ( // `onBeforeNavigate` & `onToggleHeaderArea` have to be added like this, otherwise Storybook invokes them incorrectly sometimes - +
Evangelize the UI framework across the company}> 4 days overdue - Cascaded @@ -180,11 +180,10 @@ export const Default: Story = {
- + + + ); +}; + +export const ObjectPageSingleSectionIconTabBarTestComp = () => { + return ( + + + Single section content + + + ); +}; diff --git a/packages/main/src/components/ObjectPage/test/ObjectPage.spec.tsx b/packages/main/src/components/ObjectPage/test/ObjectPage.spec.tsx index 563d44913cf..9dce0079a9b 100644 --- a/packages/main/src/components/ObjectPage/test/ObjectPage.spec.tsx +++ b/packages/main/src/components/ObjectPage/test/ObjectPage.spec.tsx @@ -145,4 +145,34 @@ test.describe('ObjectPage', () => { 'external-section-label', ); }); + + test('keeps only a single section out of the tab order', async ({ mount, page }) => { + await mount('ObjectPage/ObjectPageSingleSectionTestComp'); + + const section = page.getByRole('region', { name: 'Single section' }); + const firstSubSection = page.getByRole('region', { name: 'First subsection' }); + const secondSubSection = page.getByRole('region', { name: 'Second subsection' }); + + expect(await section.getAttribute('tabindex')).toBeNull(); + await expect(section).toHaveAttribute('aria-label', 'Single section'); + await expect(firstSubSection).toHaveAttribute('tabindex', '0'); + await expect(page.locator('[data-component-name="ObjectPageTabContainer"]')).toHaveCount(0); + + await firstSubSection.focus(); + await page.keyboard.press('ArrowDown'); + await expect(secondSubSection).toBeFocused(); + await page.keyboard.press('Tab'); + await expect(page.getByRole('button', { name: 'After ObjectPage' })).toBeFocused(); + await page.keyboard.press('Shift+Tab'); + await expect(secondSubSection).toBeFocused(); + expect(await section.getAttribute('tabindex')).toBeNull(); + + await mount('ObjectPage/ObjectPageSingleSectionIconTabBarTestComp'); + expect(await page.getByRole('region', { name: 'Single section' }).getAttribute('tabindex')).toBeNull(); + await expect(page.locator('[data-component-name="ObjectPageTabContainer"]')).toHaveCount(0); + + await mount('ObjectPage/ObjectPageFitContentTabSwitchTestComp'); + await expect(page.getByRole('region', { name: 'Fit 1' })).toHaveAttribute('tabindex', '0'); + await expect(page.locator('[data-component-name="ObjectPageTabContainer"]').first()).toBeVisible(); + }); }); diff --git a/packages/main/src/components/ObjectPageSection/index.tsx b/packages/main/src/components/ObjectPageSection/index.tsx index 9c87a8bd808..351dfd05bea 100644 --- a/packages/main/src/components/ObjectPageSection/index.tsx +++ b/packages/main/src/components/ObjectPageSection/index.tsx @@ -158,20 +158,21 @@ const ObjectPageSection = forwardRef((p ), [children], ); - const objectPageMode = useObjectPageContext(); + const { mode: objectPageMode, hasOnlySingleSection } = useObjectPageContext(); const handleFocusDefault = (e: FocusEvent) => { 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 as HTMLCollectionOf).forEach((el) => { - if (el.dataset.componentName === 'ObjectPageSection') { - el.tabIndex = -1; - } - }); - - e.currentTarget.tabIndex = 0; + if (!hasOnlySingleSection) { + // reset tab-index of all sections, so only the focused one is tabbable + Array.from(e.currentTarget.parentElement.children as HTMLCollectionOf).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') { @@ -259,7 +260,7 @@ const ObjectPageSection = forwardRef((p className, )} style={style} - tabIndex={objectPageMode === ObjectPageMode.Default ? -1 : 0} + tabIndex={hasOnlySingleSection ? undefined : objectPageMode === ObjectPageMode.Default ? -1 : 0} {...propsWithoutOmitted} aria-label={ariaLabel ?? (ariaLabelledBy == null ? titleText : undefined)} aria-labelledby={ariaLabelledBy} diff --git a/packages/main/src/components/ObjectPageSubSection/index.tsx b/packages/main/src/components/ObjectPageSubSection/index.tsx index 08498429a8a..c415d2ef81c 100644 --- a/packages/main/src/components/ObjectPageSubSection/index.tsx +++ b/packages/main/src/components/ObjectPageSubSection/index.tsx @@ -10,6 +10,7 @@ import { FlexBoxDirection } from '../../enums/FlexBoxDirection.js'; import { FlexBoxJustifyContent } from '../../enums/FlexBoxJustifyContent.js'; import type { CommonProps } from '../../types/index.js'; import { FlexBox } from '../FlexBox/index.js'; +import { useObjectPageContext } from '../ObjectPage/context.js'; import { navigateSections } from '../ObjectPage/ObjectPageUtils.js'; import { classNames, styleData } from './ObjectPageSubSection.module.css.js'; @@ -78,6 +79,7 @@ const ObjectPageSubSection = forwardRef