diff --git a/packages/main/cypress/specs/Form.cy.tsx b/packages/main/cypress/specs/Form.cy.tsx index cfc850db16bb..8e167d940ae9 100644 --- a/packages/main/cypress/specs/Form.cy.tsx +++ b/packages/main/cypress/specs/Form.cy.tsx @@ -724,9 +724,9 @@ describe("Accessibility", () => { cy.get("@form") .shadow() - .find(".ui5-form-group") + .find(".ui5-form-group-layout") .eq(0) - .as("firstGroupDOMRef"); + .as("firstGroupDefinitionList"); cy.get("@form") .shadow() @@ -740,11 +740,11 @@ describe("Accessibility", () => { cy.get("@formGroup") .should("have.attr", "data-sap-ui-fastnavgroup", "true"); - cy.get("@firstGroupDOMRef") - .should("have.attr", "role", "form"); + cy.get("@firstGroupDefinitionList") + .should("not.have.attr", "role", "form"); // assert: the form group's aria-labelledby is equal to the form group title's ID - cy.get("@firstGroupDOMRef") + cy.get("@firstGroupDefinitionList") .invoke("attr", "aria-labelledby") .then(ariaLabelledBy => { cy.get("@firstGroupTitle") @@ -861,7 +861,7 @@ describe("Accessibility", () => { cy.get("@form") .shadow() - .find(".ui5-form-group") + .find(".ui5-form-group-layout") .eq(0) .should("have.attr", "aria-label", Form.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, "1")); }); @@ -882,7 +882,7 @@ describe("Accessibility", () => { cy.get("@form") .shadow() - .find(".ui5-form-group") + .find(".ui5-form-group-layout") .eq(0) .should("have.attr", "aria-label", EXPECTED_LABEL); }); @@ -902,7 +902,7 @@ describe("Accessibility", () => { cy.get("@form") .shadow() - .find(".ui5-form-group") + .find(".ui5-form-group-layout") .eq(0) .as("group") .invoke("attr", "aria-labelledby") @@ -938,7 +938,7 @@ describe("Accessibility", () => { cy.get("@form") .shadow() - .find(".ui5-form-group") + .find(".ui5-form-group-layout") .eq(0) .as("group") .invoke("attr", "aria-labelledby") diff --git a/packages/main/src/Form.ts b/packages/main/src/Form.ts index 4eeb5f5404fb..fb269d8b4601 100644 --- a/packages/main/src/Form.ts +++ b/packages/main/src/Form.ts @@ -3,8 +3,12 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScope.js"; +import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import type { AriaRole } from "@ui5/webcomponents-base"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; + // Template import FormTemplate from "./FormTemplate.js"; @@ -12,9 +16,9 @@ import FormTemplate from "./FormTemplate.js"; import FormCss from "./generated/themes/Form.css.js"; import type FormItemSpacing from "./types/FormItemSpacing.js"; +import type FormAccessibilityMode from "./types/FormAccessibilityMode.js"; import type FormGroup from "./FormGroup.js"; import type TitleLevel from "./types/TitleLevel.js"; -import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import { FORM_ACCESSIBLE_NAME } from "./generated/i18n/i18n-defaults.js"; @@ -47,13 +51,17 @@ interface IFormItem extends UI5Element { columnSpan?: number; headerText?: string; headerLevel?: `${TitleLevel}`; + accessibilityMode?: `${FormAccessibilityMode}`; } type GroupItemsInfo = { groupItem: IFormItem, items: Array, - accessibleNameRef: string | undefined, accessibleName: string | undefined, + accessibleNameInner: string | undefined, + accessibleNameRef: string | undefined, + accessibleNameRefInner: string | undefined, + role: AriaRole | undefined, } type ItemsInfo = { @@ -222,6 +230,15 @@ class Form extends UI5Element { @property() accessibleName?: string; + /** + * Defines id (or many ids) of the element (or elements) that label the component. + * @default undefined + * @public + * @since 2.16.0 + */ + @property() + accessibleNameRef?: string; + /** * Defines the number of columns to distribute the form content by breakpoint. * @@ -294,9 +311,9 @@ class Form extends UI5Element { /** * Defines the vertical spacing between form items. * - * **Note:** If the Form is meant to be switched between "non-edit" and "edit" modes, - * we recommend using "Large" item spacing in "non-edit" mode, and "Normal" - for "edit" mode, - * to avoid "jumping" effect, caused by the hight difference between texts in "non-edit" mode and the input fields in "edit" mode. + * **Note:** If the Form is meant to be switched between "display"("non-edit") and "edit" modes, + * we recommend using "Large" item spacing in "display"("non-edit") mode, and "Normal" - for "edit" mode, + * to avoid "jumping" effect, caused by the hight difference between texts in "display"("non-edit") mode and the input fields in "edit" mode. * * @default "Normal" * @public @@ -304,6 +321,23 @@ class Form extends UI5Element { @property() itemSpacing: `${FormItemSpacing}` = "Normal"; + /** + * 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. + * + * **Usage:** + * - 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. + * + * @default "Display" + * @since 2.16.0 + * @public + */ + @property() + accessibilityMode: `${FormAccessibilityMode}` = "Display"; + /** * Defines the component header area. * @@ -373,7 +407,7 @@ class Form extends UI5Element { this.setGroupsColSpan(); // Set item spacing - this.setItemSpacing(); + this.setItemsState(); } onAfterRendering() { @@ -521,9 +555,10 @@ class Form extends UI5Element { return index === 0 ? MIN_COL_SPAN + (delta - groups) + 1 : MIN_COL_SPAN + 1; } - setItemSpacing() { + setItemsState() { this.items.forEach((item: IFormItem) => { item.itemSpacing = this.itemSpacing; + item.accessibilityMode = this.accessibilityMode; }); } @@ -544,13 +579,18 @@ class Form extends UI5Element { } get effectiveAccessibleName() { - if (this.accessibleName) { - return this.accessibleName; + if (this.accessibleName || this.accessibleNameRef) { + return getEffectiveAriaLabelText(this); } + return this.hasHeader ? undefined : Form.i18nBundle.getText(FORM_ACCESSIBLE_NAME); } get effectiveАccessibleNameRef(): string | undefined { + if (this.accessibleName || this.accessibleNameRef) { + return; + } + return this.hasHeaderText && !this.hasCustomHeader ? `${this._id}-header-text` : undefined; } @@ -582,11 +622,16 @@ class Form extends UI5Element { } }); + const accessibleNameRef = (groupItem as FormGroup).effectiveAccessibleNameRef; + return { groupItem, - accessibleName: (groupItem as FormGroup).getEffectiveAccessibleName(index), - accessibleNameRef: (groupItem as FormGroup).effectiveАccessibleNameRef, + accessibleName: this.accessibilityMode === "Edit" ? (groupItem as FormGroup).getEffectiveAccessibleName(index) : undefined, + accessibleNameInner: this.accessibilityMode === "Edit" ? undefined : (groupItem as FormGroup).getEffectiveAccessibleName(index), + accessibleNameRef: this.accessibilityMode === "Edit" ? accessibleNameRef : undefined, + accessibleNameRefInner: this.accessibilityMode === "Edit" ? undefined : accessibleNameRef, items: this.getItemsInfo((Array.from(groupItem.children) as Array)), + role: this.accessibilityMode === "Edit" ? "form" : undefined, }; }); } diff --git a/packages/main/src/FormGroup.ts b/packages/main/src/FormGroup.ts index a1de80036f66..3e9625f8859a 100644 --- a/packages/main/src/FormGroup.ts +++ b/packages/main/src/FormGroup.ts @@ -134,7 +134,7 @@ class FormGroup extends UI5Element implements IFormItem { return FormGroup.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, index + 1); } - get effectiveАccessibleNameRef() { + get effectiveAccessibleNameRef() { return this.headerText ? `${this._id}-group-header-text` : undefined; } diff --git a/packages/main/src/FormItem.ts b/packages/main/src/FormItem.ts index 054f7483babf..0cab3f784d87 100644 --- a/packages/main/src/FormItem.ts +++ b/packages/main/src/FormItem.ts @@ -12,6 +12,7 @@ import FormItemCss from "./generated/themes/FormItem.css.js"; import type { IFormItem } from "./Form.js"; import type FormItemSpacing from "./types/FormItemSpacing.js"; +import type FormAccessibilityMode from "./types/FormAccessibilityMode.js"; /** * @class @@ -85,6 +86,12 @@ class FormItem extends UI5Element implements IFormItem { @property() itemSpacing: `${FormItemSpacing}` = "Normal" + /** + * @private + */ + @property() + accessibilityMode: `${FormAccessibilityMode}` = "Display"; + get isGroup() { return false; } diff --git a/packages/main/src/FormItemTemplate.tsx b/packages/main/src/FormItemTemplate.tsx index 6afca7e86298..d779a72abf61 100644 --- a/packages/main/src/FormItemTemplate.tsx +++ b/packages/main/src/FormItemTemplate.tsx @@ -1,21 +1,42 @@ +import type { SlottedChild } from "@ui5/webcomponents-base/dist/UI5Element.js"; import type FormItem from "./FormItem.js"; -import type SlottedChild from "@ui5/webcomponents-base/dist/UI5Element.js"; export default function FormItemTemplate(this: FormItem) { return (
-
- -
-
- {this.content.map(item => -
- -
- )} -
+ { this.accessibilityMode === "Edit" ? content.call(this) : contentAsDefinitionList.call(this) }
); } + +function content(this: FormItem) { + return <> +
+ +
+
+ {this.content.map(item => +
+ +
+ )} +
+ ; +} + +function contentAsDefinitionList(this: FormItem) { + return <> +
+ +
+
+ {this.content.map(item => +
+ +
+ )} +
+ ; +} diff --git a/packages/main/src/FormTemplate.tsx b/packages/main/src/FormTemplate.tsx index 69e1753e439f..3190f7f00cb9 100644 --- a/packages/main/src/FormTemplate.tsx +++ b/packages/main/src/FormTemplate.tsx @@ -19,58 +19,74 @@ export default function FormTemplate(this: Form) { } -
- { - this.hasGroupItems ? - <> - { - this.groupItemsInfo.map(groupItemInfo => { - const groupItem = groupItemInfo.groupItem; - return ( -
-
- {groupItem.headerText && -
- {groupItem.headerText} -
- } + { this.hasGroupItems ? groupedItemsLayout.call(this) : standaloneItemsLayout.call(this) } +
+ ); +} -
- -
-
-
- ); - } - )} - +function groupedItemsLayout(this: Form) { + return
+ { this.groupItemsInfo.map(groupItemInfo => { + const groupItem = groupItemInfo.groupItem; + return
+
+ { groupItem.headerText && +
+ {groupItem.headerText} +
+ } + { this.accessibilityMode === "Edit" ? +
+ +
: - <> - { - this.itemsInfo.map(itemInfo => { - const item = itemInfo.item; - return ( -
- -
- ); - }) - } - - } +
+ +
+ } +
+
; + })} +
; +} + +function standaloneItemsLayout(this: Form) { + return ( + this.accessibilityMode === "Edit" ? +
+ { standaloneItemsLayoutContent.call(this) }
- + : +
+ { standaloneItemsLayoutContent.call(this) } +
); } + +function standaloneItemsLayoutContent(this: Form) { + return this.itemsInfo.map(itemInfo => { + const item = itemInfo.item; + return ( +
+ +
+ ); + }); +} diff --git a/packages/main/src/themes/Form.css b/packages/main/src/themes/Form.css index 4434bb83eec1..b344bb25210b 100644 --- a/packages/main/src/themes/Form.css +++ b/packages/main/src/themes/Form.css @@ -49,3 +49,8 @@ ::slotted([ui5-form-group]) { display: contents; } + +dl { + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/packages/main/src/themes/FormItem.css b/packages/main/src/themes/FormItem.css index 5ec8ba7a20c6..db419564f3a7 100644 --- a/packages/main/src/themes/FormItem.css +++ b/packages/main/src/themes/FormItem.css @@ -95,5 +95,8 @@ :host { order: var(--ui5-form-item-order-Xl, unset); } +} +dd { + margin-inline-start: 0; } \ No newline at end of file diff --git a/packages/main/src/types/FormAccessibilityMode.ts b/packages/main/src/types/FormAccessibilityMode.ts new file mode 100644 index 000000000000..4ad054341683 --- /dev/null +++ b/packages/main/src/types/FormAccessibilityMode.ts @@ -0,0 +1,28 @@ +/** + * Accessibility modes of the Form. + * + * Based on the mode, the Form and its items will render different HTML elements and ARIA attributes, + * which are appropriate for the use-case. + * + * **Usage:** + * - "Display" mode should be used when the form consists of non-editable (e.g. texts) form items. + * - "Edit" mode should be used when the form consists of editable (e.g. input fields) form items. + * + * @public + * @since 2.16.0 + */ +enum FormAccessibilityMode { + /** + * Display mode. + * @public + */ + Display = "Display", + + /** + * Edit mode. + * @public + */ + Edit = "Edit", +} + +export default FormAccessibilityMode; diff --git a/packages/main/src/types/FormItemSpacing.ts b/packages/main/src/types/FormItemSpacing.ts index cc6ffe9cc240..10b161f3e07e 100644 --- a/packages/main/src/types/FormItemSpacing.ts +++ b/packages/main/src/types/FormItemSpacing.ts @@ -1,6 +1,5 @@ /** - * Different Button designs. - * + * Different spacing of the form items. * @public * @since 2.0.0 */ diff --git a/packages/main/test/pages/form/FormA11y.html b/packages/main/test/pages/form/FormA11y.html new file mode 100644 index 000000000000..49c3212a788a --- /dev/null +++ b/packages/main/test/pages/form/FormA11y.html @@ -0,0 +1,326 @@ + + + + + + Form A11y + + + + + + + + + + + +
+ + +
+ + + Name: + Red Point Stores + + + + ZIP Code/City: + 411 Maintown + + + + Street: + Main St 1618 + + + + Country: + Germany + + +
+ +
+


+ +
+ + + + + Name: + Red Point Stores + + + + ZIP Code/City: + 411 Maintown + + + + Street: + Main St 1618 + + + + Country: + Germany + + + + WebSite: + sap.com + + + + + + Twitter: + @sap + + + + Email: + john.smith@sap.com + + + + Tel: + +49 6227 747474 + + + + SMS: + +49 6227 747474 + + + + Mobile: + +49 6227 747474 + + + + Pager: + +49 6227 747474 + + + + Fax: + +49 6227 747474 + + + + +
+ +
+


+ +
+ + + Name: + + + + + ZIP Code/City: + + + + + + Street: + + + + + + Country: + + Australia + Germany + England + + + + + WebSite: + + + + + Delivery address: + + + +
+ +
+


+ +


+
+ + + + + diff --git a/packages/main/test/pages/form/FormBasic.html b/packages/main/test/pages/form/FormBasic.html index 62b76faa9576..73424df78d7b 100644 --- a/packages/main/test/pages/form/FormBasic.html +++ b/packages/main/test/pages/form/FormBasic.html @@ -525,23 +525,23 @@
- Name: - Red Point Stores + Name: + Red Point Stores - ZIP Code/City: - 411 Maintown + ZIP Code/City: + 411 Maintown - Street: - Main St 1618 + Street: + Main St 1618 - Country: - Germany + Country: + Germany
@@ -596,22 +596,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany @@ -621,7 +621,7 @@ Delivery address: - Newtown + Newtown @@ -635,22 +635,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany @@ -662,22 +662,22 @@ Twitter: - @sap + @sap Email: - john.smith@sap.com + john.smith@sap.com Tel: - +49 6227 747474 + +49 6227 747474 SMS: - +49 6227 747474 + +49 6227 747474 @@ -708,22 +708,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany @@ -735,22 +735,22 @@ Twitter: - @sap + @sap Email: - john.smith@sap.com + john.smith@sap.com Tel: - +49 6227 747474 + +49 6227 747474 SMS: - +49 6227 747474 + +49 6227 747474 @@ -773,22 +773,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany @@ -809,22 +809,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany @@ -836,22 +836,22 @@ Twitter: - @sap + @sap Email: - john.smith@sap.com + john.smith@sap.com Tel: - +49 6227 747474 + +49 6227 747474 SMS: - +49 6227 747474 + +49 6227 747474 @@ -874,22 +874,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany @@ -909,22 +909,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany @@ -936,22 +936,22 @@ Twitter: - @sap + @sap Email: - john.smith@sap.com + john.smith@sap.com Tel: - +49 6227 747474 + +49 6227 747474 SMS: - +49 6227 747474 + +49 6227 747474 @@ -974,22 +974,22 @@ Name: - Red Point Stores + Red Point Stores ZIP Code/City: - 411 Maintown + 411 Maintown Street: - Main St 1618 + Main St 1618 Country: - Germany + Germany diff --git a/packages/website/docs/_samples/main/Form/Edit/main.js b/packages/website/docs/_samples/main/Form/Edit/main.js index 73c1d1774a71..6cd626505ffa 100644 --- a/packages/website/docs/_samples/main/Form/Edit/main.js +++ b/packages/website/docs/_samples/main/Form/Edit/main.js @@ -31,9 +31,10 @@ swEditable.addEventListener("selection-change", function () { // set itemSpacing Normal/Large to avoid jumping when switching from texts to inputs. editableForm.itemSpacing = editable ? "Normal" : "Large"; -}); - + // set accessibilityMode Edit/Display for best screen reader experience + editableForm.accessibilityMode = editable ? "Edit" : "Display"; +}); const displayTemplate = ` @@ -67,40 +68,40 @@ const displayTemplate = ` `; - const editTemplate = ` - - Name: - - - - - Country: - - Australia - Germany - England - - - - - ZIP Code/City: - - - +const editTemplate = ` + + Name: + + - - WebSite: - - - - - Street: - - - - - - Delivery address: - - - `; \ No newline at end of file + + Country: + + Australia + Germany + England + + + + + ZIP Code/City: + + + + + + WebSite: + + + + + Street: + + + + + + Delivery address: + + +`; \ No newline at end of file