diff --git a/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml b/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml
index f90080e2218..6016e1355cf 100644
--- a/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml
+++ b/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/addons/web/static/src/core/dropdown/dropdown.js b/addons/web/static/src/core/dropdown/dropdown.js
index 3a30e527aa2..95a80d24e84 100644
--- a/addons/web/static/src/core/dropdown/dropdown.js
+++ b/addons/web/static/src/core/dropdown/dropdown.js
@@ -122,6 +122,12 @@ export class Dropdown extends Component {
}
},
};
+ if (this.props.container) {
+ positioningOptions.container = () =>
+ typeof this.props.container === "function"
+ ? this.props.container()
+ : this.props.container;
+ }
this.state.directionCaretClass = DIRECTION_CARET_CLASS[direction];
this.togglerRef = useRef("togglerRef");
if (this.props.toggler === "parent") {
@@ -390,5 +396,9 @@ Dropdown.props = {
type: Boolean,
optional: true,
},
+ container: {
+ type: [Element, Function],
+ optional: true,
+ },
};
Dropdown.template = "web.Dropdown";
diff --git a/addons/web/static/src/core/position_hook.js b/addons/web/static/src/core/position_hook.js
index 53e1f8d5de7..6962aba3a14 100644
--- a/addons/web/static/src/core/position_hook.js
+++ b/addons/web/static/src/core/position_hook.js
@@ -11,7 +11,7 @@ import { onWillUnmount, useEffect, useExternalListener, useRef } from "@odoo/owl
/**
* @typedef {{
* popper?: string;
- * container?: HTMLElement;
+ * container?: HTMLElement | (() => HTMLElement);
* margin?: number;
* position?: Direction | Position;
* onPositioned?: PositionEventHandler;
@@ -92,6 +92,10 @@ function getBestPosition(reference, popper, { container, margin, position }) {
variantKey === "fit" ? FIT_FLIP_ORDER[directionKey] : DIRECTION_FLIP_ORDER[directionKey];
const variants = VARIANT_FLIP_ORDER[variantKey];
+ if (typeof container === "function") {
+ container = container();
+ }
+
// Boxes
const popBox = popper.getBoundingClientRect();
const refBox = reference.getBoundingClientRect();
diff --git a/addons/web/static/src/views/kanban/kanban_controller.js b/addons/web/static/src/views/kanban/kanban_controller.js
index e65e0ea729e..bce6c0e24d8 100644
--- a/addons/web/static/src/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/views/kanban/kanban_controller.js
@@ -35,13 +35,13 @@ export class KanbanController extends Component {
rootState,
});
- const rootRef = useRef("root");
- useViewButtons(this.model, rootRef, {
+ this.rootRef = useRef("root");
+ useViewButtons(this.model, this.rootRef, {
beforeExecuteAction: this.beforeExecuteActionButton.bind(this),
afterExecuteAction: this.afterExecuteActionButton.bind(this),
});
useSetupView({
- rootRef,
+ rootRef: this.rootRef,
getGlobalState: () => {
return {
resIds: this.model.root.records.map((rec) => rec.resId), // WOWL: ask LPE why?
@@ -121,6 +121,10 @@ export class KanbanController extends Component {
async afterExecuteActionButton(clickParams) {}
async onUpdatedPager() {}
+
+ scrollTop() {
+ this.rootRef.el.querySelector(".o_content").scrollTo({ top: 0 });
+ }
}
KanbanController.template = `web.KanbanView`;
diff --git a/addons/web/static/src/views/kanban/kanban_controller.scss b/addons/web/static/src/views/kanban/kanban_controller.scss
index 1af7bb3c790..f98899a71ed 100644
--- a/addons/web/static/src/views/kanban/kanban_controller.scss
+++ b/addons/web/static/src/views/kanban/kanban_controller.scss
@@ -60,7 +60,7 @@
&:focus,
&:focus-within {
- z-index: 1; // show the shadow on top of the previous & next cards in grouped mode
+ margin-bottom: 0; // show the shadow on top of the previous & next cards in grouped mode
}
// ------- Kanban Record, v11 Layout -------
@@ -511,9 +511,35 @@
background-color: var(--KanbanColumn__highlight-background) !important;
box-shadow: -1px 0px 0px 0px var(--KanbanColumn__highlight-border) inset,
1px 0px 0px 0px var(--KanbanColumn__highlight-border) inset;
+
+ .o_kanban_header {
+ // hack to display correct background color
+ margin: 0 calc(-1 * var(--KanbanGroup-padding-h) + 1px);
+ padding: 0 calc(var(--KanbanGroup-padding-h) - 1px);
+ }
}
}
+ .o_kanban_group .o_kanban_header,
+ .o_quick_create_folded {
+ position: sticky; // keep it visible
+ z-index: 1;
+ }
+
+ .o_kanban_group:not(.o_column_folded) .o_kanban_header {
+ background-color: $o-webclient-background-color;
+
+ .dropdown-item {
+ max-width: calc(var(--KanbanRecord-width) * 0.75);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+
+ .o_quick_create_folded {
+ top: 1rem;
+ }
+
.o_column_quick_create,
.o_kanban_group:not(.o_column_folded) .o_kanban_header_title {
// Makes them come on top of the "no-content" background gradient.
@@ -562,12 +588,6 @@
.o_column_quick_create {
scroll-snap-align: center;
overflow-y: scroll;
-
- .o_kanban_header {
- position: sticky; // keep it visible
- z-index: 1;
- background-color: inherit;
- }
}
}
diff --git a/addons/web/static/src/views/kanban/kanban_controller.xml b/addons/web/static/src/views/kanban/kanban_controller.xml
index e44abc58231..05aa60d05db 100644
--- a/addons/web/static/src/views/kanban/kanban_controller.xml
+++ b/addons/web/static/src/views/kanban/kanban_controller.xml
@@ -15,6 +15,7 @@
forceGlobalClick="props.forceGlobalClick"
openRecord.bind="openRecord"
noContentHelp="props.info.noContentHelp"
+ scrollTop.bind="scrollTop"
/>
diff --git a/addons/web/static/src/views/kanban/kanban_renderer.js b/addons/web/static/src/views/kanban/kanban_renderer.js
index 6896cf6f3a1..bc4c62ac7e2 100644
--- a/addons/web/static/src/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/views/kanban/kanban_renderer.js
@@ -25,6 +25,29 @@ const DRAGGABLE_GROUP_TYPES = ["many2one"];
const MOVABLE_RECORD_TYPES = ["char", "boolean", "integer", "selection", "many2one"];
export class KanbanRenderer extends Component {
+ static template = "web.KanbanRenderer";
+ static components = {
+ Dropdown,
+ DropdownItem,
+ ColumnProgress,
+ KanbanColumnQuickCreate,
+ KanbanRecord,
+ KanbanRecordQuickCreate,
+ };
+ static props = [
+ "archInfo",
+ "Compiler?", // optional in stable for backward compatibility
+ "list",
+ "openRecord",
+ "readonly",
+ "forceGlobalClick?",
+ "noContentHelp?",
+ "scrollTop?",
+ ];
+ static defaultProps = {
+ scrollTop: () => {},
+ };
+
setup() {
this.dialogClose = [];
this.state = useState({
@@ -40,12 +63,12 @@ export class KanbanRenderer extends Component {
// Sortable
let dataRecordId;
let dataGroupId;
- const rootRef = useRef("root");
+ this.rootRef = useRef("root");
if (this.canUseSortable) {
useSortable({
enable: () => this.canResequenceRecords,
// Params
- ref: rootRef,
+ ref: this.rootRef,
elements: ".o_record_draggable",
ignore: ".dropdown",
groups: () => this.props.list.isGrouped && ".o_kanban_group",
@@ -66,7 +89,7 @@ export class KanbanRenderer extends Component {
useSortable({
enable: () => this.canResequenceGroups,
// Params
- ref: rootRef,
+ ref: this.rootRef,
elements: ".o_group_draggable",
handle: ".o_column_title",
cursor: "move",
@@ -81,7 +104,7 @@ export class KanbanRenderer extends Component {
});
}
- useBounceButton(rootRef, (clickedEl) => {
+ useBounceButton(this.rootRef, (clickedEl) => {
if (!this.props.list.count || this.props.list.model.useSampleModel) {
return clickedEl.matches(
[
@@ -105,7 +128,7 @@ export class KanbanRenderer extends Component {
if (model.useSampleModel || !model.hasData()) {
return;
}
- const firstCard = rootRef.el.querySelector(".o_kanban_record");
+ const firstCard = this.rootRef.el.querySelector(".o_kanban_record");
if (firstCard) {
// Focus first kanban card
firstCard.focus();
@@ -127,10 +150,10 @@ export class KanbanRenderer extends Component {
}
return;
},
- { area: () => rootRef.el }
+ { area: () => this.rootRef.el }
);
- const arrowsOptions = { area: () => rootRef.el, allowRepeat: true };
+ const arrowsOptions = { area: () => this.rootRef.el, allowRepeat: true };
if (this.env.searchModel) {
useHotkey(
"ArrowUp",
@@ -260,7 +283,7 @@ export class KanbanRenderer extends Component {
if (!this.env.isSmall && group.isFolded) {
classes.push("o_column_folded");
}
- if (!group.isFolded && !group.hasActiveProgressValue) {
+ if (!group.isFolded) {
classes.push("bg-100");
}
if (group.progressBars.length) {
@@ -447,13 +470,18 @@ export class KanbanRenderer extends Component {
});
}
+ getGroupEl(groupId) {
+ return this.rootRef.el.querySelector(`.o_kanban_group[data-id="${groupId}"]`);
+ }
+
// ------------------------------------------------------------------------
// Handlers
// ------------------------------------------------------------------------
- onGroupClick(group) {
+ async onGroupClick(group, ev) {
if (!this.env.isSmall && group.isFolded) {
- group.toggle();
+ await group.toggle();
+ this.props.scrollTop();
}
}
@@ -603,22 +631,3 @@ export class KanbanRenderer extends Component {
};
}
}
-
-KanbanRenderer.props = [
- "archInfo",
- "Compiler?", // optional in stable for backward compatibility
- "list",
- "openRecord",
- "readonly",
- "forceGlobalClick?",
- "noContentHelp?",
-];
-KanbanRenderer.components = {
- Dropdown,
- DropdownItem,
- ColumnProgress,
- KanbanColumnQuickCreate,
- KanbanRecord,
- KanbanRecordQuickCreate,
-};
-KanbanRenderer.template = "web.KanbanRenderer";
diff --git a/addons/web/static/src/views/kanban/kanban_renderer.xml b/addons/web/static/src/views/kanban/kanban_renderer.xml
index 414c3a5f421..e5aa39e879c 100644
--- a/addons/web/static/src/views/kanban/kanban_renderer.xml
+++ b/addons/web/static/src/views/kanban/kanban_renderer.xml
@@ -15,10 +15,10 @@
t-on-click="(ev) => this.onGroupClick(group, ev)"
>
diff --git a/addons/web/static/tests/views/kanban_view_tests.js b/addons/web/static/tests/views/kanban_view_tests.js
index 5c13502ee02..ff302cffca1 100644
--- a/addons/web/static/tests/views/kanban_view_tests.js
+++ b/addons/web/static/tests/views/kanban_view_tests.js
@@ -12314,4 +12314,46 @@ QUnit.module("Views", (hooks) => {
);
assert.containsN(target, ".o_kanban_group .o_kanban_record", 16);
});
+
+ QUnit.test("scroll on group unfold and progressbar click", async (assert) => {
+ assert.expect(7);
+
+ await makeView({
+ type: "kanban",
+ resModel: "partner",
+ serverData,
+ arch: `
+
+
+
+ Record
+
+ `,
+ groupBy: ["product_id"],
+ async mockRPC(route, args, performRPC) {
+ if (args.method === "web_read_group") {
+ const result = await performRPC(route, args);
+ if (result.groups.length) {
+ result.groups[0].__fold = false;
+ result.groups[1].__fold = true;
+ }
+ return result;
+ }
+ },
+ });
+
+ const content = target.querySelector(".o_content");
+ content.scrollTo = (params) => {
+ assert.step("scrolled");
+ assert.strictEqual(params.top, 0);
+ };
+
+ await click(getProgressBars(0)[0]);
+ assert.verifySteps(["scrolled"]);
+
+ const column1 = getColumn(1);
+ assert.hasClass(column1, "o_column_folded");
+ await click(column1);
+ assert.verifySteps(["scrolled"]);
+ });
});