Skip to content

Commit 28994f1

Browse files
authored
feat(ui5-form): enhance a11y using description list (#12365)
Updates Form's accessibility by using description list for display use-cases. Fixes: #12156 ### add Form#аccessibleMode ("Display" | "Edit") Defines the accessibility mode of the component in "edit" and "display" use-cases. Based on the mode, the component renders different HTML elements and ARIA attributes, which are appropriate for the use-case. - Set this property to "Display", when the form consists of non-editable (e.g. texts) form items. - Set this property to "Edit", when the form consists of editable (e.g. input fields) form items.
1 parent 6344333 commit 28994f1

File tree

13 files changed

+617
-177
lines changed

13 files changed

+617
-177
lines changed

packages/main/cypress/specs/Form.cy.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -724,9 +724,9 @@ describe("Accessibility", () => {
724724

725725
cy.get("@form")
726726
.shadow()
727-
.find(".ui5-form-group")
727+
.find(".ui5-form-group-layout")
728728
.eq(0)
729-
.as("firstGroupDOMRef");
729+
.as("firstGroupDefinitionList");
730730

731731
cy.get("@form")
732732
.shadow()
@@ -740,11 +740,11 @@ describe("Accessibility", () => {
740740
cy.get("@formGroup")
741741
.should("have.attr", "data-sap-ui-fastnavgroup", "true");
742742

743-
cy.get("@firstGroupDOMRef")
744-
.should("have.attr", "role", "form");
743+
cy.get("@firstGroupDefinitionList")
744+
.should("not.have.attr", "role", "form");
745745

746746
// assert: the form group's aria-labelledby is equal to the form group title's ID
747-
cy.get("@firstGroupDOMRef")
747+
cy.get("@firstGroupDefinitionList")
748748
.invoke("attr", "aria-labelledby")
749749
.then(ariaLabelledBy => {
750750
cy.get("@firstGroupTitle")
@@ -861,7 +861,7 @@ describe("Accessibility", () => {
861861

862862
cy.get("@form")
863863
.shadow()
864-
.find(".ui5-form-group")
864+
.find(".ui5-form-group-layout")
865865
.eq(0)
866866
.should("have.attr", "aria-label", Form.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, "1"));
867867
});
@@ -882,7 +882,7 @@ describe("Accessibility", () => {
882882

883883
cy.get("@form")
884884
.shadow()
885-
.find(".ui5-form-group")
885+
.find(".ui5-form-group-layout")
886886
.eq(0)
887887
.should("have.attr", "aria-label", EXPECTED_LABEL);
888888
});
@@ -902,7 +902,7 @@ describe("Accessibility", () => {
902902

903903
cy.get("@form")
904904
.shadow()
905-
.find(".ui5-form-group")
905+
.find(".ui5-form-group-layout")
906906
.eq(0)
907907
.as("group")
908908
.invoke("attr", "aria-labelledby")
@@ -938,7 +938,7 @@ describe("Accessibility", () => {
938938

939939
cy.get("@form")
940940
.shadow()
941-
.find(".ui5-form-group")
941+
.find(".ui5-form-group-layout")
942942
.eq(0)
943943
.as("group")
944944
.invoke("attr", "aria-labelledby")

packages/main/src/Form.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
55
import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScope.js";
66
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
77
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
8+
import type { AriaRole } from "@ui5/webcomponents-base";
9+
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
10+
811
// Template
912
import FormTemplate from "./FormTemplate.js";
1013

1114
// Styles
1215
import FormCss from "./generated/themes/Form.css.js";
1316

1417
import type FormItemSpacing from "./types/FormItemSpacing.js";
18+
import type FormAccessibleMode from "./types/FormAccessibleMode.js";
1519
import type FormGroup from "./FormGroup.js";
1620
import type TitleLevel from "./types/TitleLevel.js";
17-
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
1821

1922
import { FORM_ACCESSIBLE_NAME } from "./generated/i18n/i18n-defaults.js";
2023

@@ -47,13 +50,17 @@ interface IFormItem extends UI5Element {
4750
columnSpan?: number;
4851
headerText?: string;
4952
headerLevel?: `${TitleLevel}`;
53+
accessibleMode?: `${FormAccessibleMode}`;
5054
}
5155

5256
type GroupItemsInfo = {
5357
groupItem: IFormItem,
5458
items: Array<ItemsInfo>,
55-
accessibleNameRef: string | undefined,
5659
accessibleName: string | undefined,
60+
accessibleNameInner: string | undefined,
61+
accessibleNameRef: string | undefined,
62+
accessibleNameRefInner: string | undefined,
63+
role: AriaRole | undefined,
5764
}
5865

5966
type ItemsInfo = {
@@ -222,6 +229,23 @@ class Form extends UI5Element {
222229
@property()
223230
accessibleName?: string;
224231

232+
/**
233+
* Defines the accessibility mode of the component in "edit" and "display" use-cases.
234+
*
235+
* Based on the mode, the component renders different HTML elements and ARIA attributes,
236+
* which are appropriate for the use-case.
237+
*
238+
* **Usage:**
239+
* - Set this property to "Display", when the form consists of non-editable (e.g. texts) form items.
240+
* - Set this property to "Edit", when the form consists of editable (e.g. input fields) form items.
241+
*
242+
* @default "Display"
243+
* @since 2.16.0
244+
* @public
245+
*/
246+
@property()
247+
accessibleMode: `${FormAccessibleMode}` = "Display";
248+
225249
/**
226250
* Defines the number of columns to distribute the form content by breakpoint.
227251
*
@@ -294,9 +318,9 @@ class Form extends UI5Element {
294318
/**
295319
* Defines the vertical spacing between form items.
296320
*
297-
* **Note:** If the Form is meant to be switched between "non-edit" and "edit" modes,
298-
* we recommend using "Large" item spacing in "non-edit" mode, and "Normal" - for "edit" mode,
299-
* to avoid "jumping" effect, caused by the hight difference between texts in "non-edit" mode and the input fields in "edit" mode.
321+
* **Note:** If the Form is meant to be switched between "display"("non-edit") and "edit" modes,
322+
* we recommend using "Large" item spacing in "display"("non-edit") mode, and "Normal" - for "edit" mode,
323+
* to avoid "jumping" effect, caused by the hight difference between texts in "display"("non-edit") mode and the input fields in "edit" mode.
300324
*
301325
* @default "Normal"
302326
* @public
@@ -373,7 +397,7 @@ class Form extends UI5Element {
373397
this.setGroupsColSpan();
374398

375399
// Set item spacing
376-
this.setItemSpacing();
400+
this.setItemsState();
377401
}
378402

379403
onAfterRendering() {
@@ -521,9 +545,10 @@ class Form extends UI5Element {
521545
return index === 0 ? MIN_COL_SPAN + (delta - groups) + 1 : MIN_COL_SPAN + 1;
522546
}
523547

524-
setItemSpacing() {
548+
setItemsState() {
525549
this.items.forEach((item: IFormItem) => {
526550
item.itemSpacing = this.itemSpacing;
551+
item.accessibleMode = this.accessibleMode;
527552
});
528553
}
529554

@@ -547,10 +572,15 @@ class Form extends UI5Element {
547572
if (this.accessibleName) {
548573
return this.accessibleName;
549574
}
575+
550576
return this.hasHeader ? undefined : Form.i18nBundle.getText(FORM_ACCESSIBLE_NAME);
551577
}
552578

553579
get effectiveАccessibleNameRef(): string | undefined {
580+
if (this.accessibleName) {
581+
return;
582+
}
583+
554584
return this.hasHeaderText && !this.hasCustomHeader ? `${this._id}-header-text` : undefined;
555585
}
556586

@@ -582,11 +612,16 @@ class Form extends UI5Element {
582612
}
583613
});
584614

615+
const accessibleNameRef = (groupItem as FormGroup).effectiveAccessibleNameRef;
616+
585617
return {
586618
groupItem,
587-
accessibleName: (groupItem as FormGroup).getEffectiveAccessibleName(index),
588-
accessibleNameRef: (groupItem as FormGroup).effectiveАccessibleNameRef,
619+
accessibleName: this.accessibleMode === "Edit" ? (groupItem as FormGroup).getEffectiveAccessibleName(index) : undefined,
620+
accessibleNameInner: this.accessibleMode === "Edit" ? undefined : (groupItem as FormGroup).getEffectiveAccessibleName(index),
621+
accessibleNameRef: this.accessibleMode === "Edit" ? accessibleNameRef : undefined,
622+
accessibleNameRefInner: this.accessibleMode === "Edit" ? undefined : accessibleNameRef,
589623
items: this.getItemsInfo((Array.from(groupItem.children) as Array<IFormItem>)),
624+
role: this.accessibleMode === "Edit" ? "form" : undefined,
590625
};
591626
});
592627
}

packages/main/src/FormGroup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class FormGroup extends UI5Element implements IFormItem {
134134
return FormGroup.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, index + 1);
135135
}
136136

137-
get effectiveАccessibleNameRef() {
137+
get effectiveAccessibleNameRef() {
138138
return this.headerText ? `${this._id}-group-header-text` : undefined;
139139
}
140140

packages/main/src/FormItem.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import FormItemCss from "./generated/themes/FormItem.css.js";
1212

1313
import type { IFormItem } from "./Form.js";
1414
import type FormItemSpacing from "./types/FormItemSpacing.js";
15+
import type FormAccessibleMode from "./types/FormAccessibleMode.js";
1516

1617
/**
1718
* @class
@@ -85,6 +86,12 @@ class FormItem extends UI5Element implements IFormItem {
8586
@property()
8687
itemSpacing: `${FormItemSpacing}` = "Normal"
8788

89+
/**
90+
* @private
91+
*/
92+
@property()
93+
accessibleMode: `${FormAccessibleMode}` = "Display";
94+
8895
get isGroup() {
8996
return false;
9097
}
Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
1+
import type { SlottedChild } from "@ui5/webcomponents-base/dist/UI5Element.js";
12
import type FormItem from "./FormItem.js";
2-
import type SlottedChild from "@ui5/webcomponents-base/dist/UI5Element.js";
33

44
export default function FormItemTemplate(this: FormItem) {
55
return (
66
<div class="ui5-form-item-root">
77
<div class="ui5-form-item-layout" part="layout">
8-
<div class="ui5-form-item-label" part="label">
9-
<slot name="labelContent"></slot>
10-
</div>
11-
<div class="ui5-form-item-content" part="content">
12-
{this.content.map(item =>
13-
<div class="ui5-form-item-content-child">
14-
<slot name={(item as SlottedChild)._individualSlot}></slot>
15-
</div>
16-
)}
17-
</div>
8+
{ this.accessibleMode === "Edit" ? content.call(this) : contentAsDefinitionList.call(this) }
189
</div>
1910
</div>
2011
);
2112
}
13+
14+
function content(this: FormItem) {
15+
return <>
16+
<div class="ui5-form-item-label" part="label">
17+
<slot name="labelContent"></slot>
18+
</div>
19+
<div class="ui5-form-item-content" part="content">
20+
{this.content.map(item =>
21+
<div class="ui5-form-item-content-child">
22+
<slot name={(item as SlottedChild)._individualSlot}></slot>
23+
</div>
24+
)}
25+
</div>
26+
</>;
27+
}
28+
29+
function contentAsDefinitionList(this: FormItem) {
30+
return <>
31+
<dt class="ui5-form-item-label" part="label">
32+
<slot name="labelContent"></slot>
33+
</dt>
34+
<dd class="ui5-form-item-content" part="content">
35+
{this.content.map(item =>
36+
<div class="ui5-form-item-content-child">
37+
<slot name={(item as SlottedChild)._individualSlot}></slot>
38+
</div>
39+
)}
40+
</dd>
41+
</>;
42+
}

0 commit comments

Comments
 (0)