diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/README.md b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/README.md
index d3221a18b1..79a8b3042e 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/README.md
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/README.md
@@ -22,6 +22,8 @@ Adaptive Form Table component written in HTL that allows authors to capture data
* Ability to contain `tableheader` and `tablerow` child components
* Configurable proportional column widths via comma-separated values
* Optional column sorting with ascending/descending toggle (per-column sort can be individually disabled)
+* Mobile card layout — at `max-width: 768px` rows stack into cards; column headers are shown as inline labels via CSS `::before` using `data-label` attributes stamped on each `
`
+* Mobile action bar — Sort and Filter overlays accessible via bottom-sheet UI on narrow viewports; Sort requires `enableSorting`
* Document of Record (DoR) support — table structure and column widths exported for XFA-based DoR rendering
* Short description / long description / question mark help pattern
* Visible and enabled state binding for rules engine
@@ -68,6 +70,18 @@ BLOCK cmp-adaptiveform-table
ELEMENT cmp-adaptiveform-tablerow__runtime-controls
ELEMENT cmp-adaptiveform-tablerow__add-button
ELEMENT cmp-adaptiveform-tablerow__remove-button
+ ELEMENT cmp-adaptiveform-table__mobile-bar (injected by JS; hidden on desktop)
+ ELEMENT cmp-adaptiveform-table__mobile-bar-btn
+ MODIFIER cmp-adaptiveform-table__mobile-bar-btn--sort
+ MODIFIER cmp-adaptiveform-table__mobile-bar-btn--filter
+ ELEMENT cmp-adaptiveform-table__mobile-bar-divider
+ ELEMENT cmp-adaptiveform-table__sort-overlay (shared by sort and filter bottom-sheet overlays)
+ ELEMENT cmp-adaptiveform-table__sort-sheet
+ ELEMENT cmp-adaptiveform-table__sort-sheet-handle
+ ELEMENT cmp-adaptiveform-table__sort-sheet-title
+ ELEMENT cmp-adaptiveform-table__sort-options
+ ELEMENT cmp-adaptiveform-table__sort-option
+ ELEMENT cmp-adaptiveform-table__sort-option-indicator
```
## Theme Editor Support
@@ -94,7 +108,10 @@ The following attributes are required for initialization:
The following are optional attributes that can be added to the component:
1. `data-cmp-visible` - boolean indicating whether the component is currently visible
2. `data-cmp-enabled` - boolean indicating whether the component is currently enabled
-3. `data-cmp-sorting-enabled` - set to `"true"` when `./enableSorting` is authored; controls sort button rendering in `tableheader.html`
+3. `data-cmp-sorting-enabled` - set to `"true"` when `./enableSorting` is authored; controls sort button rendering in `tableheader.html` and enables the Sort button in the mobile action bar
+
+The following attribute is stamped by JavaScript on each `
` in the table body at runtime:
+1. `data-label` - set to the corresponding column header text; used by CSS `::before` to render inline labels in the mobile card layout (no author action required)
## Information
* **Vendor**: Adobe
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/clientlibs/site/js/tableview.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/clientlibs/site/js/tableview.js
index daca571477..81f8a063f9 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/clientlibs/site/js/tableview.js
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/table/v1/table/clientlibs/site/js/tableview.js
@@ -34,12 +34,18 @@
this.children = [];
/** @type {{ col: number, dir: 'asc'|'desc' }|null} */
this._tableSortState = null;
+ /** @type {HTMLElement|null} */
+ this._mobileSortOverlay = null;
+ /** @type {HTMLElement|null} */
+ this._mobileFilterOverlay = null;
}
setModel(model) {
super.setModel(model);
queueMicrotask(() => {
this.#initColumnSortingIfEnabled();
+ this.#stampMobileLabels();
+ this.#initMobileSortBar();
});
}
@@ -113,6 +119,47 @@
this.updateLabel(state.label);
}
+ /**
+ * Mobile card layout (CSS-driven, via @media max-width:768px) stacks each
+ * row into a card and renders the column header as a ::before label sourced
+ * from each cell's data-label attribute. This method is the single point that
+ * stamps that attribute: it reads the header text from each
and copies
+ * it onto the matching column index in every body
.
+ *
+ * Layout itself is pure CSS — this only supplies the label text. It runs on
+ * init and again after rows are added so dynamically-cloned rows are covered.
+ * It never touches field state, visibility, or DOM structure.
+ *
+ * @param {HTMLElement} [scope] - Optional row element to limit stamping to
+ * (used after a single row is added); defaults to the whole tbody.
+ */
+ #stampMobileLabels(scope) {
+ const widget = this.element.querySelector(Table.selectors.widget);
+ if (!widget) {
+ return;
+ }
+ const thead = widget.querySelector("thead");
+ const tbody = widget.querySelector("tbody");
+ if (!thead || !tbody) {
+ return;
+ }
+ const headers = Array.from(thead.querySelectorAll("th.cmp-adaptiveform-tablehead"))
+ .map((th) => th.innerText.replace(/\s+/g, " ").trim());
+ if (headers.length === 0) {
+ return;
+ }
+ const rows = scope && scope.matches && scope.matches("tr")
+ ? [scope]
+ : Array.from(tbody.querySelectorAll(":scope > tr"));
+ rows.forEach((row) => {
+ Array.from(row.cells).forEach((cell, index) => {
+ if (index < headers.length && headers[index]) {
+ cell.setAttribute("data-label", headers[index]);
+ }
+ });
+ });
+ }
+
/**
* Get the
element for row insertion.
*/
@@ -177,6 +224,9 @@
// the wrong model index when clicked.
this.#syncTableRowHooks(htmlElement, addedModel.id);
+ // Stamp mobile card labels on the freshly added row.
+ this.#stampMobileLabels(htmlElement);
+
return htmlElement;
}
@@ -263,13 +313,13 @@
* @param {HTMLTableSectionElement} thead
* @param {number} colIndex
*/
- #sortTableByColumn(tbody, thead, colIndex) {
+ #sortTableByColumn(tbody, thead, colIndex, forceDir = null) {
const rows = Array.from(tbody.querySelectorAll(":scope > tr"));
if (rows.length <= 1) {
return;
}
- let dir = "asc";
- if (this._tableSortState && this._tableSortState.col === colIndex) {
+ let dir = forceDir ?? "asc";
+ if (!forceDir && this._tableSortState && this._tableSortState.col === colIndex) {
dir = this._tableSortState.dir === "asc" ? "desc" : "asc";
}
this._tableSortState = { col: colIndex, dir: dir };
@@ -321,6 +371,196 @@
}
}
+ /**
+ * Builds the mobile action bar (Sort | Filter) inserted before the table widget.
+ * Bar is hidden on desktop via CSS. Overlays appended to document.body so
+ * position:fixed works freely. Sort requires enableSorting; filter is always on.
+ */
+ #initMobileSortBar() {
+ const widget = this.element.querySelector(Table.selectors.widget);
+ if (!widget) return;
+ const thead = widget.querySelector('thead');
+ const tbody = widget.querySelector('tbody');
+ if (!thead || !tbody) return;
+
+ const esc = (s) => s.replace(/&/g, '&').replace(//g, '>');
+ const headers = Array.from(thead.querySelectorAll('th.cmp-adaptiveform-tablehead'))
+ .map((th) => th.innerText.replace(/\s+/g, ' ').trim());
+ if (headers.length === 0) return;
+
+ const sortingEnabled = this.element.dataset.cmpSortingEnabled === 'true';
+
+ // --- Action bar (inserted BEFORE the widget) ---
+ const bar = document.createElement('div');
+ bar.className = 'cmp-adaptiveform-table__mobile-bar';
+ bar.innerHTML = `
+
+
+ `;
+ widget.before(bar);
+
+ // --- Sort overlay ---
+ if (sortingEnabled) {
+ const optionsHtml = headers.map((h, i) => `
+