Skip to content
Merged
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
116 changes: 115 additions & 1 deletion packages/main/cypress/specs/Form.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import FormItem from "../../src/FormItem.js";
import FormGroup from "../../src/FormGroup.js";
import Label from "../../src/Label.js";
import Text from "../../src/Text.js";
import Title from "../../src/Title.js";
import Input from "../../src/Input.js";
import { FORM_GROUP_ACCESSIBLE_NAME } from "../../src/generated/i18n/i18n-defaults.js";

describe("General API", () => {
it("tests calculated state of Form with default layout, label-span and empty-span", () => {
Expand Down Expand Up @@ -845,6 +845,120 @@ describe("Accessibility", () => {
.should("have.attr", "aria-label", "basic form");
});

describe("FormGroup accessibility", () => {
it("tests 'aria-label' default", () => {
cy.mount(<Form>
<FormGroup>
<FormItem>
<Label>Name:</Label>
<Text>Red Point Stores</Text>
</FormItem>
</FormGroup>
</Form>);

cy.get("[ui5-form]")
.as("form");

cy.get("@form")
.shadow()
.find(".ui5-form-group")
.eq(0)
.should("have.attr", "aria-label", Form.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, "1"));
});

it("tests 'aria-label' via accessible-name", () => {
const EXPECTED_LABEL = "Custom group label";
cy.mount(<Form>
<FormGroup accessibleName={EXPECTED_LABEL}>
<FormItem>
<Label>Name:</Label>
<Text>Red Point Stores</Text>
</FormItem>
</FormGroup>
</Form>);

cy.get("[ui5-form]")
.as("form");

cy.get("@form")
.shadow()
.find(".ui5-form-group")
.eq(0)
.should("have.attr", "aria-label", EXPECTED_LABEL);
});

it("tests 'aria-labelledby' via header-text", () => {
cy.mount(<Form>
<FormGroup headerText="Custom header text">
<FormItem>
<Label>Name:</Label>
<Text>Red Point Stores</Text>
</FormItem>
</FormGroup>
</Form>);

cy.get("[ui5-form]")
.as("form");

cy.get("@form")
.shadow()
.find(".ui5-form-group")
.eq(0)
.as("group")
.invoke("attr", "aria-labelledby")
.then(ariaLabelledBy => {
cy.get("@form")
.shadow()
.find(".ui5-form-group")
.eq(0)
.find(".ui5-form-group-heading [ui5-title]")
.invoke("attr", "id")
.should(id => {
expect(ariaLabelledBy).to.equal(id);
});
});

cy.get("@group")
.should("not.have.attr", "aria-label");
});

it("tests 'aria-label' via accessible-name and header-text", () => {
const EXPECTED_LABEL = "Custom group header";
cy.mount(<Form>
<FormGroup headerText="Custom header text" accessibleName={EXPECTED_LABEL}>
<FormItem>
<Label>Name:</Label>
<Text>Red Point Stores</Text>
</FormItem>
</FormGroup>
</Form>);

cy.get("[ui5-form]")
.as("form");

cy.get("@form")
.shadow()
.find(".ui5-form-group")
.eq(0)
.as("group")
.invoke("attr", "aria-labelledby")
.then(ariaLabelledBy => {
cy.get("@form")
.shadow()
.find(".ui5-form-group")
.eq(0)
.find(".ui5-form-group-heading [ui5-title]")
.invoke("attr", "id")
.should(id => {
expect(ariaLabelledBy).to.equal(id);
});
});

cy.get("@group")
.should("have.attr", "aria-label", EXPECTED_LABEL);
});
});

it("tests F6 navigation", () => {
cy.mount(
<>
Expand Down
8 changes: 5 additions & 3 deletions packages/main/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ interface IFormItem extends UI5Element {
type GroupItemsInfo = {
groupItem: IFormItem,
items: Array<ItemsInfo>,
accessibleNameRef: string | undefined
accessibleNameRef: string | undefined,
accessibleName: string | undefined,
}

type ItemsInfo = {
Expand Down Expand Up @@ -558,7 +559,7 @@ class Form extends UI5Element {
}

get groupItemsInfo(): Array<GroupItemsInfo> {
return this.items.map((groupItem: IFormItem) => {
return this.items.map((groupItem: IFormItem, index: number) => {
const items = this.getItemsInfo((Array.from(groupItem.children) as Array<IFormItem>));
breakpoints.forEach(breakpoint => {
const cols = ((groupItem[`cols${breakpoint}` as keyof IFormItem]) as number || 1);
Expand All @@ -583,7 +584,8 @@ class Form extends UI5Element {

return {
groupItem,
accessibleNameRef: (groupItem as FormGroup).headerText ? `${groupItem._id}-group-header-text` : undefined,
accessibleName: (groupItem as FormGroup).getEffectiveAccessibleName(index),
accessibleNameRef: (groupItem as FormGroup).effectiveАccessibleNameRef,
items: this.getItemsInfo((Array.from(groupItem.children) as Array<IFormItem>)),
};
});
Expand Down
32 changes: 32 additions & 0 deletions packages/main/src/FormGroup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
Expand All @@ -8,6 +10,8 @@ import type { IFormItem } from "./Form.js";
import type FormItemSpacing from "./types/FormItemSpacing.js";
import type TitleLevel from "./types/TitleLevel.js";

import { FORM_GROUP_ACCESSIBLE_NAME } from "./generated/i18n/i18n-defaults.js";

/**
* @class
*
Expand Down Expand Up @@ -68,6 +72,15 @@ class FormGroup extends UI5Element implements IFormItem {
@property({ type: Number })
columnSpan?: number;

/**
* Defines the accessible ARIA name of the component.
* @default undefined
* @public
* @since 2.16.0
*/
@property()
accessibleName?: string;

/**
* Defines the items of the component.
* @public
Expand Down Expand Up @@ -96,6 +109,9 @@ class FormGroup extends UI5Element implements IFormItem {
@property()
itemSpacing: `${FormItemSpacing}` = "Normal";

@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;

onBeforeRendering() {
this.processFormItems();
}
Expand All @@ -106,6 +122,22 @@ class FormGroup extends UI5Element implements IFormItem {
});
}

getEffectiveAccessibleName(index: number) {
if (this.accessibleName) {
return this.accessibleName;
}

if (this.headerText) {
return undefined;
}

return FormGroup.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, index + 1);
}

get effectiveАccessibleNameRef() {
return this.headerText ? `${this._id}-group-header-text` : undefined;
}

get isGroup() {
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/FormTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function FormTemplate(this: Form) {
}}
part="column"
>
<div class="ui5-form-group" role="form" aria-labelledby={groupItemInfo.accessibleNameRef}>
<div class="ui5-form-group" role="form" aria-labelledby={groupItemInfo.accessibleNameRef} aria-label={groupItemInfo.accessibleName}>
{groupItem.headerText &&
<div class="ui5-form-group-heading">
<Title id={`${groupItem._id}-group-header-text`} level={groupItem.headerLevel} size="H6">{groupItem.headerText}</Title>
Expand Down
3 changes: 3 additions & 0 deletions packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,9 @@ TOOLBAR_POPOVER_AVAILABLE_VALUES=Available Values
#XACT: ARIA announcement for the Form aria-label attribute
FORM_ACCESSIBLE_NAME=Form

#XACT: ARIA announcement for the Form group aria-label attribute
FORM_GROUP_ACCESSIBLE_NAME=Group {0}

#XMSG: Text used for reporting that a radio button group requires one of the radio buttons to be checked
FORM_CHECKABLE_REQUIRED=Please tick this box if you want to proceed.

Expand Down
Loading