Skip to content

Commit

Permalink
feat(ui5-form): enable vertical alignment of form items (#10165)
Browse files Browse the repository at this point in the history
This PR introduces support for two navigation flows: a simple layout with only ui5-form-item elements and a grouped layout with ui5-form-group elements.

When ui5-form is used with only ui5-form-item elements, the focus moves horizontally across each ui5-form-item, as there may be forms with custom arrangements. In this case, each ui5-form-item is treated as a separate group.

When ui5-form is used with ui5-form-group elements to create groups, the items are rendered by filling each column first, then filling the rows. In this case, the focus moves vertically through each item, column by column.

Fixes: #10032
  • Loading branch information
nnaydenow authored Nov 15, 2024
1 parent cff55e2 commit 13b571b
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 1 deletion.
241 changes: 241 additions & 0 deletions packages/main/cypress/specs/Form.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,247 @@ describe("General API", () => {
cy.get("@formGr3")
.should("have.prop", "colsXl", 1);
});

describe("tests items ordering within a group", () => {
beforeEach(() => {
cy.mount(html`<ui5-form layout="S3 M4 L5 XL6">
<ui5-form-group>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>1</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>2</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>3</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>4</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>5</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>6</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>7</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>8</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>9</span>
</ui5-form-item>
<ui5-form-item>
<span slot="labelContent">Item:</span>
<span>10</span>
</ui5-form-item>
</ui5-form-group>
</ui5-form>`);
});

it("10 items in 6 columns", () => {
cy.get("[ui5-form]")
.invoke("width", 1500);

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

cy.get("@items")
.eq(0)
.should("have.css", "order", "0");

cy.get("@items")
.eq(1)
.should("have.css", "order", "6");

cy.get("@items")
.eq(2)
.should("have.css", "order", "1");

cy.get("@items")
.eq(3)
.should("have.css", "order", "7");

cy.get("@items")
.eq(4)
.should("have.css", "order", "2");

cy.get("@items")
.eq(5)
.should("have.css", "order", "8");

cy.get("@items")
.eq(6)
.should("have.css", "order", "3");

cy.get("@items")
.eq(7)
.should("have.css", "order", "9");

cy.get("@items")
.eq(8)
.should("have.css", "order", "4");

cy.get("@items")
.eq(9)
.should("have.css", "order", "5");
});

it("10 items in 5 columns", () => {
cy.get("[ui5-form]")
.invoke("width", 1300);

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

cy.get("@items")
.eq(0)
.should("have.css", "order", "0");

cy.get("@items")
.eq(1)
.should("have.css", "order", "5");

cy.get("@items")
.eq(2)
.should("have.css", "order", "1");

cy.get("@items")
.eq(3)
.should("have.css", "order", "6");

cy.get("@items")
.eq(4)
.should("have.css", "order", "2");

cy.get("@items")
.eq(5)
.should("have.css", "order", "7");

cy.get("@items")
.eq(6)
.should("have.css", "order", "3");

cy.get("@items")
.eq(7)
.should("have.css", "order", "8");

cy.get("@items")
.eq(8)
.should("have.css", "order", "4");

cy.get("@items")
.eq(9)
.should("have.css", "order", "9");
});

it("10 items in 4 columns", () => {
cy.get("[ui5-form]")
.invoke("width", 800);

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

cy.get("@items")
.eq(0)
.should("have.css", "order", "0");

cy.get("@items")
.eq(1)
.should("have.css", "order", "4");

cy.get("@items")
.eq(2)
.should("have.css", "order", "8");

cy.get("@items")
.eq(3)
.should("have.css", "order", "1");

cy.get("@items")
.eq(4)
.should("have.css", "order", "5");

cy.get("@items")
.eq(5)
.should("have.css", "order", "9");

cy.get("@items")
.eq(6)
.should("have.css", "order", "2");

cy.get("@items")
.eq(7)
.should("have.css", "order", "6");

cy.get("@items")
.eq(8)
.should("have.css", "order", "3");

cy.get("@items")
.eq(9)
.should("have.css", "order", "7");
});

it("10 items in 3 columns", () => {
cy.get("[ui5-form]")
.invoke("width", 500);

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

cy.get("@items")
.eq(0)
.should("have.css", "order", "0");

cy.get("@items")
.eq(1)
.should("have.css", "order", "3");

cy.get("@items")
.eq(2)
.should("have.css", "order", "6");

cy.get("@items")
.eq(3)
.should("have.css", "order", "9");

cy.get("@items")
.eq(4)
.should("have.css", "order", "1");

cy.get("@items")
.eq(5)
.should("have.css", "order", "4");

cy.get("@items")
.eq(6)
.should("have.css", "order", "7");

cy.get("@items")
.eq(7)
.should("have.css", "order", "2");

cy.get("@items")
.eq(8)
.should("have.css", "order", "5");

cy.get("@items")
.eq(9)
.should("have.css", "order", "8");
});
});
});

describe("Accessibility", () => {
Expand Down
59 changes: 59 additions & 0 deletions packages/main/src/Form.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import { getScopedVarName } from "@ui5/webcomponents-base/dist/CustomElementsScope.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";

Expand All @@ -23,6 +24,8 @@ const StepColumn = {
"XL": 6,
};

const breakpoints = ["S", "M", "L", "Xl"];

/**
* Interface for components that can be slotted inside `ui5-form` as items.
* @public
Expand Down Expand Up @@ -127,6 +130,40 @@ type ItemsInfo = {
*
* **For example:** To always place the labels on top set: `labelSpan="S12 M12 L12 XL12"` property.
*
* ### Navigation flow
*
* The Form component supports two layout options for keyboard navigation:
*
* #### Simple form
*
* In this "simple form" layout, each `ui5-form-item` acts as a standalone group
* with one item, so focus moves horizontally across the grid from one `ui5-form-item` to the next.
* This layout is ideal for simpler forms and supports custom arrangements, e.g.,
*
* ```
* | 1 | 2 |
* | 3 |
* | 4 | 5 |
* ```
*
* #### Complex form
*
* In this layout, items are grouped into `ui5-form-group` elements, allowing more complex configurations:
*
* - **Single-Column Group**: Focus moves vertically down from one item to the next.
* ```
* | 1 |
* | 2 |
* | 3 |
* ```
*
* - **Multi-Column Group**: Focus moves horizontally within each row, advancing to the next row after completing the current one.
* ```
* | 1 | 4 |
* | 2 | 5 |
* | 3 | 6 |
* ```
*
* ### Keyboard Handling
*
* - [Tab] - Moves the focus to the next interactive element within the Form/FormGroup (if available) or to the next element in the tab chain outside the Form
Expand Down Expand Up @@ -389,6 +426,28 @@ class Form extends UI5Element {

get groupItemsInfo(): Array<GroupItemsInfo> {
return this.items.map((groupItem: IFormItem) => {
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);
const rows = Math.ceil(items.length / cols);
const total = cols * rows;
const lastRowColumns = (cols - (total - items.length) - 1); // all other indecies start from 0
let currentItem = 0;

for (let i = 0; i < total; i++) {
const column = Math.floor(i / rows);
const row = i % rows;

if (row === rows - 1 && column > lastRowColumns) {
// eslint-disable-next-line no-continue
continue;
}

items[currentItem].item.style.setProperty(getScopedVarName(`--ui5-form-item-order-${breakpoint}`), `${column + row * cols}`);
currentItem++;
}
});

return {
groupItem,
accessibleNameRef: (groupItem as FormGroup).headerText ? `${groupItem._id}-group-header-text` : undefined,
Expand Down
32 changes: 31 additions & 1 deletion packages/main/src/themes/FormItem.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
:host([column-span="3"]) {
grid-column: span 3;
}

:host([column-span="4"]) {
grid-column: span 4;
}
Expand Down Expand Up @@ -62,4 +62,34 @@
}
::slotted([ui5-select]) {
width: 100%;
}


@container (max-width: 600px) {
:host {
order: var(--ui5-form-item-order-S, unset);
}
}

/* M - 1 column by default, up to 2 columns */
@container (width > 600px) and (width <= 1024px) {
:host {
order: var(--ui5-form-item-order-M, unset);
}

}

/* L - 2 columns by default, up to 3 columns */
@container (width > 1024px) and (width <= 1440px) {
:host {
order: var(--ui5-form-item-order-L, unset);
}
}

/* XL - 3 columns by default, up to 6 */
@container (min-width: 1441px) {
:host {
order: var(--ui5-form-item-order-Xl, unset);
}

}
Loading

0 comments on commit 13b571b

Please sign in to comment.