diff --git a/.changeset/semantic-header-headings.md b/.changeset/semantic-header-headings.md new file mode 100644 index 00000000000..8157f75a5fa --- /dev/null +++ b/.changeset/semantic-header-headings.md @@ -0,0 +1,5 @@ +--- +'@siemens/ix': patch +--- + +Render `ix-application-header` and `ix-content-header` titles as semantic headings to improve document structure and accessibility. diff --git a/packages/core/src/components/application-header/application-header.scss b/packages/core/src/components/application-header/application-header.scss index 0aa506510db..77f94b765f0 100644 --- a/packages/core/src/components/application-header/application-header.scss +++ b/packages/core/src/components/application-header/application-header.scss @@ -8,6 +8,7 @@ */ @use 'mixins/shadow-dom/component'; @use 'mixins/break-points'; +@use 'mixins/fonts'; :host { display: flex; @@ -102,6 +103,10 @@ } .application-name { + @include fonts.typography-body-lg; + + margin: 0; + padding: 0; flex: 0 1 auto; min-width: 0; flex-shrink: 0; diff --git a/packages/core/src/components/application-header/application-header.tsx b/packages/core/src/components/application-header/application-header.tsx index 444cd92e66a..227f02f774e 100644 --- a/packages/core/src/components/application-header/application-header.tsx +++ b/packages/core/src/components/application-header/application-header.tsx @@ -406,9 +406,7 @@ export class ApplicationHeader { >
- - {this.name} - + {this.name &&

{this.name}

} {this.nameSuffix && this.breakpoint !== 'sm' && ( {this.nameSuffix} diff --git a/packages/core/src/components/application-header/test/application-header.ct.ts b/packages/core/src/components/application-header/test/application-header.ct.ts index fe6f973410c..47e826cde58 100644 --- a/packages/core/src/components/application-header/test/application-header.ct.ts +++ b/packages/core/src/components/application-header/test/application-header.ct.ts @@ -11,6 +11,34 @@ import { test, viewPorts } from '@utils/test'; import { ApplicationLayoutContext } from '../../utils/application-layout/context'; import { ContextType } from '../../utils/context'; +test('accessibility', async ({ mount, makeAxeBuilder }) => { + await mount( + `` + ); + + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); +}); + +test('renders application name as h1', async ({ mount, page }) => { + await mount( + `` + ); + + const heading = page.getByRole('heading', { + level: 1, + name: 'Test Application', + }); + await expect(heading).toBeVisible(); +}); + +test('does not render h1 when name is omitted', async ({ mount, page }) => { + await mount(``); + + const heading = page.locator('ix-application-header').locator('h1'); + await expect(heading).toHaveCount(0); +}); + test('renders', async ({ mount, page }) => { page.setViewportSize({ height: 500, diff --git a/packages/core/src/components/content-header/content-header.scss b/packages/core/src/components/content-header/content-header.scss index b973297e24f..868278a53f3 100644 --- a/packages/core/src/components/content-header/content-header.scss +++ b/packages/core/src/components/content-header/content-header.scss @@ -7,6 +7,9 @@ * LICENSE file in the root directory of this source tree. */ +@use 'mixins/fonts'; +@use 'misc/common-variables' as vars; + :host { display: flex; flex-direction: row; @@ -26,6 +29,19 @@ text-overflow: ellipsis; } + .header-title { + @include fonts.typography-h3; + + margin: 0; + padding: 0; + + &.secondary { + @include fonts.typography-h4; + + padding: vars.$tiny-space 0; + } + } + .headerTitleRow { display: flex; @@ -34,10 +50,6 @@ margin-left: 0.5rem; } } - - .secondary { - padding: 0.25rem 0; - } } .subtitle { diff --git a/packages/core/src/components/content-header/content-header.tsx b/packages/core/src/components/content-header/content-header.tsx index ef271ac3f9e..91cf73d4730 100644 --- a/packages/core/src/components/content-header/content-header.tsx +++ b/packages/core/src/components/content-header/content-header.tsx @@ -60,15 +60,17 @@ export class ContentHeader {
- - {this.headerTitle} - + {this.headerTitle && ( +

+ {this.headerTitle} +

+ )}
diff --git a/packages/core/src/components/content-header/test/content-header.ct.ts b/packages/core/src/components/content-header/test/content-header.ct.ts new file mode 100644 index 00000000000..c369057a0bc --- /dev/null +++ b/packages/core/src/components/content-header/test/content-header.ct.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import { expect } from '@playwright/test'; +import { regressionTest } from '@utils/test'; + +regressionTest('accessibility', async ({ mount, makeAxeBuilder }) => { + await mount( + `` + ); + + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); +}); + +regressionTest('renders', async ({ mount, page }) => { + await mount( + `` + ); + + const element = page.locator('ix-content-header'); + await expect(element).toHaveClass(/\bhydrated\b/); + await expect(element).toBeVisible(); +}); + +const variants = [ + { variant: '', expected: 'primary', hasSecondaryClass: false }, + { + variant: 'variant="secondary"', + expected: 'secondary', + hasSecondaryClass: true, + }, +]; + +for (const { variant, expected, hasSecondaryClass } of variants) { + regressionTest( + `renders header title as h2 for ${expected} variant`, + async ({ mount, page }) => { + await mount( + `` + ); + + const heading = page.getByRole('heading', { + level: 2, + name: 'My Content Page', + }); + await expect(heading).toBeVisible(); + + const titleElement = page + .locator('ix-content-header') + .locator('h2.header-title'); + if (hasSecondaryClass) { + await expect(titleElement).toHaveClass(/\bsecondary\b/); + } else { + await expect(titleElement).not.toHaveClass(/\bsecondary\b/); + } + } + ); +} + +regressionTest( + 'does not render h2 when headerTitle is omitted', + async ({ mount, page }) => { + await mount(``); + + const heading = page.locator('ix-content-header').locator('h2'); + await expect(heading).toHaveCount(0); + } +);