Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
57ec38f
chore: progress
ilhan007 Sep 24, 2025
bcf0adb
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 24, 2025
87c367b
chore: update
ilhan007 Sep 24, 2025
8c8f138
chore: lint
ilhan007 Sep 24, 2025
407f881
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 24, 2025
4cffe49
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
3430845
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
21b3db2
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
3efb0d3
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
e5d3d15
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
952c90e
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
5b8d4bf
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
99444eb
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
8992e37
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 26, 2025
b7db621
chore: add comment
ilhan007 Sep 26, 2025
30e8c3b
chore: use dd,dt tags instead of role term, definition
ilhan007 Sep 29, 2025
61c9392
chore: lint
ilhan007 Sep 29, 2025
db91033
Merge branch 'main' into feat-form-desc-list
ilhan007 Sep 30, 2025
e10b27e
chore: revert typo
ilhan007 Sep 30, 2025
0f37060
feat: add accessibleNameRef to Form
ilhan007 Oct 2, 2025
3a7bfb5
Merge branch 'main' into feat-form-desc-list
ilhan007 Oct 15, 2025
2e39eda
chore: update acc test
ilhan007 Oct 17, 2025
7a7fecc
Merge branch 'main' into feat-form-desc-list
ilhan007 Oct 17, 2025
f010e03
chore: lint
ilhan007 Oct 17, 2025
350ea8c
Merge branch 'main' into feat-form-desc-list
ilhan007 Oct 17, 2025
773eebf
chore: introduce editable prop
ilhan007 Oct 17, 2025
edd810b
chore: adapt tests
ilhan007 Oct 17, 2025
e1ddec3
Merge branch 'main' into feat-form-desc-list
ilhan007 Oct 17, 2025
d7ce78f
chore: update website sample
ilhan007 Oct 17, 2025
7280dbf
chore: rename editable to accessibilityMode
ilhan007 Oct 17, 2025
bec71e5
Merge branch 'main' into feat-form-desc-list
ilhan007 Oct 17, 2025
8a7eb5a
chore: lint
ilhan007 Oct 17, 2025
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
18 changes: 9 additions & 9 deletions packages/main/cypress/specs/Form.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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")
Expand Down Expand Up @@ -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"));
});
Expand All @@ -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);
});
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
67 changes: 56 additions & 11 deletions packages/main/src/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ 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";

// Styles
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";

Expand Down Expand Up @@ -47,13 +51,17 @@ interface IFormItem extends UI5Element {
columnSpan?: number;
headerText?: string;
headerLevel?: `${TitleLevel}`;
accessibilityMode?: `${FormAccessibilityMode}`;
}

type GroupItemsInfo = {
groupItem: IFormItem,
items: Array<ItemsInfo>,
accessibleNameRef: string | undefined,
accessibleName: string | undefined,
accessibleNameInner: string | undefined,
accessibleNameRef: string | undefined,
accessibleNameRefInner: string | undefined,
role: AriaRole | undefined,
}

type ItemsInfo = {
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -294,16 +311,33 @@ 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
*/
@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.
*
Expand Down Expand Up @@ -373,7 +407,7 @@ class Form extends UI5Element {
this.setGroupsColSpan();

// Set item spacing
this.setItemSpacing();
this.setItemsState();
}

onAfterRendering() {
Expand Down Expand Up @@ -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;
});
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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<IFormItem>)),
role: this.accessibilityMode === "Edit" ? "form" : undefined,
};
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/FormGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
7 changes: 7 additions & 0 deletions packages/main/src/FormItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -85,6 +86,12 @@ class FormItem extends UI5Element implements IFormItem {
@property()
itemSpacing: `${FormItemSpacing}` = "Normal"

/**
* @private
*/
@property()
accessibilityMode: `${FormAccessibilityMode}` = "Display";

get isGroup() {
return false;
}
Expand Down
43 changes: 32 additions & 11 deletions packages/main/src/FormItemTemplate.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div class="ui5-form-item-root">
<div class="ui5-form-item-layout" part="layout">
<div class="ui5-form-item-label" part="label">
<slot name="labelContent"></slot>
</div>
<div class="ui5-form-item-content" part="content">
{this.content.map(item =>
<div class="ui5-form-item-content-child">
<slot name={(item as SlottedChild)._individualSlot}></slot>
</div>
)}
</div>
{ this.accessibilityMode === "Edit" ? content.call(this) : contentAsDefinitionList.call(this) }
</div>
</div>
);
}

function content(this: FormItem) {
return <>
<div class="ui5-form-item-label" part="label">
<slot name="labelContent"></slot>
</div>
<div class="ui5-form-item-content" part="content">
{this.content.map(item =>
<div class="ui5-form-item-content-child">
<slot name={(item as SlottedChild)._individualSlot}></slot>
</div>
)}
</div>
</>;
}

function contentAsDefinitionList(this: FormItem) {
return <>
<dt class="ui5-form-item-label" part="label">
<slot name="labelContent"></slot>
</dt>
<dd class="ui5-form-item-content" part="content">
{this.content.map(item =>
<div class="ui5-form-item-content-child">
<slot name={(item as SlottedChild)._individualSlot}></slot>
</div>
)}
</dd>
</>;
}
Loading
Loading