Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
handleToolbarIntersection,
setHeaderActive,
setToolbarBackgroundOpacity,
getRoleType,
} from './header.utils';

/**
Expand Down Expand Up @@ -210,7 +211,7 @@ export class Header implements ComponentInterface {
const collapse = this.collapse || 'none';

// banner role must be at top level, so remove role if inside a menu
const roleType = hostContext('ion-menu', this.el) ? 'none' : 'banner';
const roleType = getRoleType(hostContext('ion-menu', this.el));

return (
<Host
Expand Down
23 changes: 23 additions & 0 deletions core/src/components/header/header.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { readTask, writeTask } from '@stencil/core';
import { clamp } from '@utils/helpers';

const TRANSITION = 'all 0.2s ease-in-out';
const ROLE_NONE = 'none';
const ROLE_BANNER = 'banner';

interface HeaderIndex {
el: HTMLIonHeaderElement;
Expand Down Expand Up @@ -171,6 +173,7 @@ export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);

if (active) {
headerEl.setAttribute('role', ROLE_BANNER);
headerEl.classList.remove('header-collapse-condense-inactive');

ionTitles.forEach((ionTitle) => {
Expand All @@ -179,6 +182,16 @@ export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
}
});
} else {
/**
* There can only be one banner landmark per page.
* By default, all ion-headers have the banner role.
* This causes an accessibility issue when using a
* condensed header since there are two ion-headers
* on the page at once (active and inactive).
* To solve this, the role needs to be toggled
* based on which header is active.
*/
headerEl.setAttribute('role', ROLE_NONE);
headerEl.classList.add('header-collapse-condense-inactive');

/**
Expand Down Expand Up @@ -244,3 +257,13 @@ export const handleHeaderFade = (scrollEl: HTMLElement, baseEl: HTMLElement, con
});
});
};

/**
* Get the role type for the ion-header.
*
* @param isInsideMenu If ion-header is inside ion-menu.
* @returns 'none' if inside ion-menu, otherwise 'banner'.
*/
export const getRoleType = (isInsideMenu: boolean) => {
return isInsideMenu ? ROLE_NONE : ROLE_BANNER;
};
18 changes: 18 additions & 0 deletions core/src/components/header/test/condense/header.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,23 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c

await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
});

test('should only have the banner role on the active header', async ({ page }) => {
await page.goto('/src/components/header/test/condense', config);
const largeTitleHeader = page.locator('#largeTitleHeader');
const smallTitleHeader = page.locator('#smallTitleHeader');
const content = page.locator('ion-content');

await expect(largeTitleHeader).toHaveAttribute('role', 'banner');
await expect(smallTitleHeader).toHaveAttribute('role', 'none');

await content.evaluate(async (el: HTMLIonContentElement) => {
await el.scrollToBottom();
});
await page.locator('#largeTitleHeader.header-collapse-condense-inactive').waitFor();

await expect(largeTitleHeader).toHaveAttribute('role', 'none');
await expect(smallTitleHeader).toHaveAttribute('role', 'banner');
});
});
});
Loading