Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
28 changes: 0 additions & 28 deletions apps/e2e-harness/e2e/config/a11y-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,34 +249,6 @@
"suppress": ["aria-dialog-name"],
"reason": "Known violations: aria-dialog-name"
},
"platform/wizard-generator/customizable-embeded": {
"suppress": ["aria-dialog-name"],
"reason": "fdp-input-message-group popover opened by wizard init focus has role=dialog with no accessible name — tracked in #14260"
},
"platform/wizard-generator/external-navigation": {
"suppress": ["aria-dialog-name"],
"reason": "fdp-input-message-group popover opened by wizard init focus has role=dialog with no accessible name — tracked in #14260"
},
"platform/wizard-generator/onchange": {
"suppress": ["aria-dialog-name"],
"reason": "fdp-input-message-group popover opened by wizard init focus has role=dialog with no accessible name — tracked in #14260"
},
"platform/wizard-generator/responsive-paddings": {
"suppress": ["aria-dialog-name"],
"reason": "fdp-input-message-group popover opened by wizard init focus has role=dialog with no accessible name — tracked in #14260"
},
"platform/wizard-generator/special-elements": {
"suppress": ["aria-dialog-name"],
"reason": "fdp-input-message-group popover opened by wizard init focus has role=dialog with no accessible name — tracked in #14260"
},
"platform/wizard-generator/summary-objects": {
"suppress": ["aria-dialog-name"],
"reason": "fdp-input-message-group popover opened by wizard init focus has role=dialog with no accessible name — tracked in #14260"
},
"platform/wizard-generator/visible-summary": {
"suppress": ["aria-dialog-name"],
"reason": "fdp-input-message-group popover opened by wizard init focus has role=dialog with no accessible name — tracked in #14260"
},
"core/tree/lazily-loaded-tree-items": {
"suppress": ["aria-treeitem-name"],
"reason": "Known violations: aria-treeitem-name"
Expand Down
7 changes: 7 additions & 0 deletions apps/e2e-harness/e2e/config/e2e.routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -2633,6 +2633,13 @@
"example": "new-placement",
"className": "PopoverCdkPlacementExampleComponent"
},
{
"path": "core/popover/non-dialog",
"library": "core",
"component": "popover",
"example": "non-dialog",
"className": "PopoverNonDialogExampleComponent"
},
{
"path": "core/popover/placement",
"library": "core",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions apps/e2e-harness/src/app/app.routes.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2635,6 +2635,13 @@ export const generatedRoutes: Routes = [
'../../../../libs/docs/core/popover/examples/popover-new-placement/popover-cdk-placement-example.component'
).then((m) => m.PopoverCdkPlacementExampleComponent)
},
{
path: 'core/popover/non-dialog',
loadComponent: () =>
import(
'../../../../libs/docs/core/popover/examples/popover-non-dialog/popover-non-dialog-example.component'
).then((m) => m.PopoverNonDialogExampleComponent)
},
{
path: 'core/popover/placement',
loadComponent: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
[focusAutoCapture]="false"
[focusTrapped]="false"
[restoreFocusOnClose]="false"
[bodyAriaLabel]="_popoverAriaLabel()"
(isOpenChange)="openChanged($event)"
>
<fd-popover-control>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,36 @@ describe('FormInputMessageGroupComponent — placementContainer: self', () => {
expect(document.activeElement).toBe(input2);
}));
});

describe('FormInputMessageGroupComponent — popover aria-label (#14260)', () => {
let fixture: ComponentFixture<TwoGroupsTestComponent>;
let hostEl: HTMLElement;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TwoGroupsTestComponent]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TwoGroupsTestComponent);
fixture.detectChanges();
hostEl = fixture.nativeElement;
});

it('renders the popover body with a non-empty aria-label', fakeAsync(() => {
const input1: HTMLInputElement = hostEl.querySelector('#input1')!;

input1.focus();
input1.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
fixture.detectChanges();
tick();
fixture.detectChanges();

const body = document.querySelector('.cdk-overlay-container .fd-popover__body');
expect(body?.getAttribute('role')).toBe('dialog');
const label = body?.getAttribute('aria-label');
expect(label).toBeTruthy();
expect(label?.length).toBeGreaterThan(0);
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@fundamental-ngx/core/popover';

import { Placement, PopoverFillMode } from '@fundamental-ngx/core/shared';
import { resolveTranslationSignalFn } from '@fundamental-ngx/i18n';

@Component({
selector: 'fd-form-input-message-group',
Expand Down Expand Up @@ -96,6 +97,9 @@ export class FormInputMessageGroupComponent {
/** @hidden */
readonly _elementRef = inject(ElementRef);

/** @hidden Translated aria-label for the popover body (#14260). */
protected readonly _popoverAriaLabel = resolveTranslationSignalFn()('coreFormInputMessageGroup.popoverAriaLabel');

/**
* Function is called every time message changes isOpen attribute
*/
Expand Down
8 changes: 6 additions & 2 deletions libs/core/popover/base/popover-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ConnectedPosition, ScrollStrategy } from '@angular/cdk/overlay';
import { ElementRef } from '@angular/core';
import { Nullable } from '@fundamental-ngx/cdk/utils';
import { Placement, PopoverFillMode } from '@fundamental-ngx/core/shared';
import { PopoverBodyRole } from '../popover.component';

/**
* Configuration object for popover trigger events.
Expand Down Expand Up @@ -137,12 +138,15 @@ export interface PopoverConfig {
/** Whether the popover body is resizable. */
resizable?: boolean;

/** ARIA role for the popover body. */
bodyRole?: string | null;
/** ARIA role for the popover body. Common values: 'dialog', 'region', 'menu', 'listbox', 'tooltip', 'alertdialog'. */
bodyRole?: PopoverBodyRole | (string & {}) | null;

/** ARIA label for the popover body. */
bodyAriaLabel?: string | null;

/** ID of the element that labels the popover body. */
bodyAriaLabelledBy?: string | null;

/** ID for the popover body. */
bodyId?: string | null;
}
1 change: 1 addition & 0 deletions libs/core/popover/popover-body/popover-body.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
[style.width.px]="_popoverBodyWidth()"
[attr.role]="_bodyRole()"
[attr.aria-label]="_bodyAriaLabel()"
[attr.aria-labelledby]="_bodyAriaLabelledBy()"
[attr.id]="_bodyId()"
fdkResize
[fdkResizeDisabled]="!_resizable()"
Expand Down
3 changes: 3 additions & 0 deletions libs/core/popover/popover-body/popover-body.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export class PopoverBodyComponent {
/** @hidden Aria label for the popover body. */
readonly _bodyAriaLabel = signal<Nullable<string>>(null);

/** @hidden ID of the element that labels the popover body. */
readonly _bodyAriaLabelledBy = signal<string | null>(null);

/** @hidden ID for the popover body. */
readonly _bodyId = signal<string | null>(null);

Expand Down
11 changes: 11 additions & 0 deletions libs/core/popover/popover-service/popover.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export class PopoverService {
/** @hidden Aria label for the popover body. */
protected _bodyAriaLabel: string | null = null;

/** @hidden ID of the element that labels the popover body. */
protected _bodyAriaLabelledBy: string | null = null;

/** @hidden ID for the popover body. */
protected readonly _bodyId = signal<string | null>(null);

Expand Down Expand Up @@ -524,12 +527,19 @@ export class PopoverService {
}
if (config.bodyRole !== undefined) {
this._bodyRole = unwrap(config.bodyRole);
this._getPopoverBody()?._bodyRole.set(this._bodyRole);
}
if (config.bodyAriaLabel !== undefined) {
this._bodyAriaLabel = unwrap(config.bodyAriaLabel);
this._getPopoverBody()?._bodyAriaLabel.set(this._bodyAriaLabel);
}
if (config.bodyAriaLabelledBy !== undefined) {
this._bodyAriaLabelledBy = unwrap(config.bodyAriaLabelledBy);
this._getPopoverBody()?._bodyAriaLabelledBy.set(this._bodyAriaLabelledBy);
}
if (config.bodyId !== undefined) {
this._bodyId.set(unwrap(config.bodyId));
this._getPopoverBody()?._bodyId.set(this._bodyId());
}

if (config.isOpen !== undefined) {
Expand Down Expand Up @@ -908,6 +918,7 @@ export class PopoverService {
body._closeOnEscapeKey.set(this.closeOnEscapeKey());
body._bodyRole.set(this._bodyRole);
body._bodyAriaLabel.set(this._bodyAriaLabel);
body._bodyAriaLabelledBy.set(this._bodyAriaLabelledBy);
body._bodyId.set(this._bodyId());
body._resizable.set(this.resizable());
body._setBodyComponentClasses(this.additionalBodyComponentClasses());
Expand Down
130 changes: 129 additions & 1 deletion libs/core/popover/popover.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ class TestPopoverComponent {
</fd-popover-body>
</fd-popover>
`,
standalone: true,
imports: [PopoverModule]
})
class TestPopoverConfigComponent {
Expand Down Expand Up @@ -516,6 +515,135 @@ describe('PopoverComponent service stub tests', () => {
});
});

@Component({
selector: 'fd-popover-body-role-test',
template: `
<fd-popover #popover [bodyRole]="bodyRole" [bodyAriaLabelledBy]="bodyAriaLabelledBy">
<fd-popover-control>
<button>Open Popover</button>
</fd-popover-control>
<fd-popover-body>
<div>Popover Content</div>
</fd-popover-body>
</fd-popover>
`,
standalone: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe no need to set standalone: true

imports: [PopoverModule]
})
class TestPopoverBodyRoleComponent {
@ViewChild('popover') popover: PopoverComponent;
bodyRole: string | null = 'dialog';
bodyAriaLabelledBy: string | null = null;
}

describe('PopoverComponent bodyRole and bodyAriaLabelledBy inputs (#14260)', () => {
let fixture: ComponentFixture<TestPopoverBodyRoleComponent>;
let hostComponent: TestPopoverBodyRoleComponent;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TestPopoverBodyRoleComponent]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestPopoverBodyRoleComponent);
hostComponent = fixture.componentInstance;
fixture.detectChanges();
});

it('defaults bodyRole to "dialog" for back-compat', fakeAsync(() => {
hostComponent.popover.open();
fixture.detectChanges();
tick();

const body = document.querySelector('.cdk-overlay-container .fd-popover__body');
expect(body?.getAttribute('role')).toBe('dialog');
}));

it('binds [bodyRole]="region" to popover-body [attr.role]', fakeAsync(() => {
hostComponent.bodyRole = 'region';
fixture.detectChanges();

hostComponent.popover.open();
fixture.detectChanges();
tick();

expect(document.querySelector('.cdk-overlay-container .fd-popover__body')?.getAttribute('role')).toBe('region');
}));

it('binds [bodyRole]="null" so no role attribute is rendered', fakeAsync(() => {
hostComponent.bodyRole = null;
fixture.detectChanges();

hostComponent.popover.open();
fixture.detectChanges();
tick();

expect(document.querySelector('.cdk-overlay-container .fd-popover__body')?.hasAttribute('role')).toBe(false);
}));

it('binds [bodyAriaLabelledBy] to popover-body [attr.aria-labelledby]', fakeAsync(() => {
hostComponent.bodyAriaLabelledBy = 'my-label-id';
fixture.detectChanges();

hostComponent.popover.open();
fixture.detectChanges();
tick();

expect(
document.querySelector('.cdk-overlay-container .fd-popover__body')?.getAttribute('aria-labelledby')
).toBe('my-label-id');
}));

it('accepts PopoverBodyRole union values (type safety test)', fakeAsync(() => {
// Test that all enumerated union values are accepted
const validRoles: Array<'dialog' | 'region' | 'menu' | 'listbox' | 'tooltip' | 'alertdialog'> = [
'dialog',
'region',
'menu',
'listbox',
'tooltip',
'alertdialog'
];

validRoles.forEach((role) => {
hostComponent.bodyRole = role;
fixture.detectChanges();

hostComponent.popover.open();
fixture.detectChanges();
tick();

expect(document.querySelector('.cdk-overlay-container .fd-popover__body')?.getAttribute('role')).toBe(role);

hostComponent.popover.close();
fixture.detectChanges();
tick();
});
}));

it('accepts arbitrary ARIA role strings via escape hatch (type safety test)', fakeAsync(() => {
// Test that non-union valid ARIA roles are still accepted
const edgeCaseRoles = ['tree', 'status', 'alert', 'presentation', 'none'];

edgeCaseRoles.forEach((role) => {
hostComponent.bodyRole = role;
fixture.detectChanges();

hostComponent.popover.open();
fixture.detectChanges();
tick();

expect(document.querySelector('.cdk-overlay-container .fd-popover__body')?.getAttribute('role')).toBe(role);

hostComponent.popover.close();
fixture.detectChanges();
tick();
});
}));
});

class PopoverServiceStub {
// Use actual signals for realistic testing
isOpen = signal(false);
Expand Down
Loading
Loading