🐛 Fix problems with padding multiple values in plugins and UI (#10602)

* 🐛 Fix problem with padding types in plugins

* 🐛 Fix problem with multiple selection paddings
This commit is contained in:
Alonso Torres 2026-07-10 09:13:20 +02:00 committed by GitHub
parent 508569437a
commit 3ee5b50007
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 388 additions and 40 deletions

View File

@ -347,6 +347,22 @@
(+ pad-top pad-top)
(+ pad-top pad-bottom))))
(defn padding-type-for
"`:simple` when top≈bottom and left≈right, `:multiple` otherwise (nil sides = 0)."
[{:keys [p1 p2 p3 p4]}]
(if (and (mth/close? (d/nilv p1 0) (d/nilv p3 0))
(mth/close? (d/nilv p2 0) (d/nilv p4 0)))
:simple
:multiple))
(defn margin-type-for
"`:simple` when top≈bottom and left≈right, `:multiple` otherwise (nil sides = 0)."
[{:keys [m1 m2 m3 m4]}]
(if (and (mth/close? (d/nilv m1 0) (d/nilv m3 0))
(mth/close? (d/nilv m2 0) (d/nilv m4 0)))
:simple
:multiple))
(defn child-min-width
[child]
(if (and (fill-width? child)

View File

@ -1213,6 +1213,10 @@
(and (= type :simple) (or (= prop :p2) (= prop #{:p2 :p4})))
(st/emit! (dwsl/update-layout ids {:layout-padding {:p2 val :p4 val}}))
(and (= type :multiple) (some? prop))
(st/emit! (dwsl/update-layout ids {:layout-padding-type :multiple
:layout-padding {prop val}}))
(some? prop)
(st/emit! (dwsl/update-layout ids {:layout-padding {prop val}}))))))

View File

@ -12,6 +12,7 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.types.component :as ctk]
[app.common.types.path :as path]
[app.common.types.shape.attrs :refer [editable-attrs]]
@ -208,6 +209,59 @@
[v]
(when v (select-keys v blur-keys)))
(def layout-padding-attrs [:p1 :p2 :p3 :p4])
(defn- normalize-layout-padding
[value]
(if (= value :multiple)
(zipmap layout-padding-attrs (repeat :multiple))
value))
(defn- merge-layout-padding
[values shape-values]
(let [current (normalize-layout-padding (get values :layout-padding ::unset))
next (normalize-layout-padding (get shape-values :layout-padding ::unset))]
(cond
(= current ::unset) next
(= next ::unset) current
(and (map? current) (map? next))
(attrs/get-attrs-multi [current next] layout-padding-attrs)
(= current next)
current
:else
:multiple)))
(defn- same-padding-value?
[v1 v2]
(if (and (number? v1) (number? v2))
(mth/close? v1 v2)
(= v1 v2)))
(defn- simple-layout-padding?
[{:keys [p1 p2 p3 p4]}]
(and (same-padding-value? p1 p3)
(same-padding-value? p2 p4)))
(defn- promote-simple-layout-padding-type
[{:keys [layout-padding layout-padding-type] :as values}]
(cond-> values
(and (= layout-padding-type :simple)
(map? layout-padding)
(not (simple-layout-padding? layout-padding)))
(assoc :layout-padding-type :multiple)))
(defn- merge-layout-container-attrs
[values shape-values attrs]
(let [merged-values (attrs/get-attrs-multi [values shape-values] attrs)
merged-values (cond-> merged-values
(or (contains? values :layout-padding)
(contains? shape-values :layout-padding))
(assoc :layout-padding (merge-layout-padding values shape-values)))]
(promote-simple-layout-padding-type merged-values)))
(defn get-attrs*
"Given a group of attributes that we want to extract and the shapes to extract them from
returns a list of tuples [id, values] with the extracted properties for the shapes that
@ -217,9 +271,10 @@
merge-attrs
(fn [v1 v2]
(cond
(= attr-group :shadow) (attrs/get-attrs-multi [v1 v2] attrs shadow-eq shadow-sel)
(= attr-group :blur) (attrs/get-attrs-multi [v1 v2] attrs blur-eq blur-sel)
:else (attrs/get-attrs-multi [v1 v2] attrs)))
(= attr-group :shadow) (attrs/get-attrs-multi [v1 v2] attrs shadow-eq shadow-sel)
(= attr-group :blur) (attrs/get-attrs-multi [v1 v2] attrs blur-eq blur-sel)
(= attr-group :layout-container) (merge-layout-container-attrs v1 v2 attrs)
:else (attrs/get-attrs-multi [v1 v2] attrs)))
merge-attr
(fn [acc applied-tokens t-attr]

View File

@ -20,6 +20,20 @@
;; Define in `app.plugins.shape` we do this way to prevent circular dependency
(def shape-proxy? nil)
(defn- update-padding
"Patch the layout padding and re-derive its `:simple`/`:multiple` type."
[this id patch]
(let [padding (merge (-> this u/proxy->shape :layout-padding) patch)]
(st/emit! (dwsl/update-layout #{id} {:layout-padding patch
:layout-padding-type (ctl/padding-type-for padding)}))))
(defn- update-margin
"Patch the item margin and re-derive its `:simple`/`:multiple` type."
[this id patch]
(let [margin (merge (-> this u/proxy->shape :layout-item-margin) patch)]
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin patch
:layout-item-margin-type (ctl/margin-type-for margin)}))))
(defn flex-layout-proxy? [p]
(obj/type-of? p "FlexLayoutProxy"))
@ -185,7 +199,7 @@
{:this true
:get #(-> % u/proxy->shape :layout-padding :p1 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :verticalPadding value)
@ -197,13 +211,13 @@
(u/not-valid plugin-id :verticalPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
(update-padding this id {:p1 value :p3 value})))}
:horizontalPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p2 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :horizontalPadding value)
@ -215,13 +229,13 @@
(u/not-valid plugin-id :horizontalPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
(update-padding this id {:p2 value :p4 value})))}
:topPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p1 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :topPadding value)
@ -233,13 +247,13 @@
(u/not-valid plugin-id :topPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
(update-padding this id {:p1 value})))}
:rightPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p2 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :rightPadding value)
@ -251,13 +265,13 @@
(u/not-valid plugin-id :rightPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
(update-padding this id {:p2 value})))}
:bottomPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p3 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :bottomPadding value)
@ -269,13 +283,13 @@
(u/not-valid plugin-id :bottomPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
(update-padding this id {:p3 value})))}
:leftPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p4 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :leftPadding value)
@ -287,7 +301,27 @@
(u/not-valid plugin-id :leftPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
(update-padding this id {:p4 value})))}
;; `:simple` mirrors vertical/horizontal padding; `:multiple` honours each side.
:paddingType
{:this true
:get #(-> % u/proxy->shape :layout-padding-type (d/nilv :simple) d/name)
:set
(fn [_ value]
(let [value (keyword value)]
(cond
(not (contains? ctl/padding-type value))
(u/not-valid plugin-id :paddingType value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :paddingType "Plugin doesn't have 'content:write' permission")
(not (u/page-active? page-id))
(u/not-valid plugin-id :paddingType "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding-type value})))))}
:remove
(fn []
@ -469,7 +503,7 @@
{:this true
:get #(-> % u/proxy->shape :layout-item-margin :m1 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :verticalMargin value)
@ -481,13 +515,13 @@
(u/not-valid plugin-id :verticalMargin "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value :m3 value}}))))}
(update-margin this id {:m1 value :m3 value})))}
:horizontalMargin
{:this true
:get #(-> % u/proxy->shape :layout-item-margin :m2 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :horizontalMargin value)
@ -499,13 +533,13 @@
(u/not-valid plugin-id :horizontalMargin "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value :m4 value}}))))}
(update-margin this id {:m2 value :m4 value})))}
:topMargin
{:this true
:get #(-> % u/proxy->shape :layout-item-margin :m1 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :topMargin value)
@ -517,13 +551,13 @@
(u/not-valid plugin-id :topMargin "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value}}))))}
(update-margin this id {:m1 value})))}
:rightMargin
{:this true
:get #(-> % u/proxy->shape :layout-item-margin :m2 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :rightMargin value)
@ -535,13 +569,13 @@
(u/not-valid plugin-id :rightMargin "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value}}))))}
(update-margin this id {:m2 value})))}
:bottomMargin
{:this true
:get #(-> % u/proxy->shape :layout-item-margin :m3 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :bottomMargin value)
@ -553,13 +587,13 @@
(u/not-valid plugin-id :bottomMargin "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m3 value}}))))}
(update-margin this id {:m3 value})))}
:leftMargin
{:this true
:get #(-> % u/proxy->shape :layout-item-margin :m4 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :leftMargin value)
@ -571,7 +605,27 @@
(u/not-valid plugin-id :leftMargin "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m4 value}}))))}
(update-margin this id {:m4 value})))}
;; `:simple` mirrors vertical/horizontal margin; `:multiple` honours each side.
:marginType
{:this true
:get #(-> % u/proxy->shape :layout-item-margin-type (d/nilv :simple) d/name)
:set
(fn [_ value]
(let [value (keyword value)]
(cond
(not (contains? ctl/item-margin-types value))
(u/not-valid plugin-id :marginType value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :marginType "Plugin doesn't have 'content:write' permission")
(not (u/page-active? page-id))
(u/not-valid plugin-id :marginType "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin-type value})))))}
:maxWidth
{:this true

View File

@ -21,6 +21,13 @@
;; Define in `app.plugins.shape` we do this way to prevent circular dependency
(def shape-proxy? nil)
(defn- update-padding
"Patch the layout padding and re-derive its `:simple`/`:multiple` type."
[this id patch]
(let [padding (merge (-> this u/proxy->shape :layout-padding) patch)]
(st/emit! (dwsl/update-layout #{id} {:layout-padding patch
:layout-padding-type (ctl/padding-type-for padding)}))))
(defn grid-layout-proxy? [p]
(obj/type-of? p "GridLayoutProxy"))
@ -217,7 +224,7 @@
{:this true
:get #(-> % u/proxy->shape :layout-padding :p1 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :verticalPadding value)
@ -229,13 +236,13 @@
(u/not-valid plugin-id :verticalPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
(update-padding this id {:p1 value :p3 value})))}
:horizontalPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p2 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :horizontalPadding value)
@ -247,13 +254,13 @@
(u/not-valid plugin-id :horizontalPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
(update-padding this id {:p2 value :p4 value})))}
:topPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p1 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :topPadding value)
@ -265,13 +272,13 @@
(u/not-valid plugin-id :topPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
(update-padding this id {:p1 value})))}
:rightPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p2 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :rightPadding value)
@ -283,13 +290,13 @@
(u/not-valid plugin-id :rightPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
(update-padding this id {:p2 value})))}
:bottomPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p3 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :bottomPadding value)
@ -301,13 +308,13 @@
(u/not-valid plugin-id :bottomPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
(update-padding this id {:p3 value})))}
:leftPadding
{:this true
:get #(-> % u/proxy->shape :layout-padding :p4 (d/nilv 0))
:set
(fn [_ value]
(fn [this value]
(cond
(not (sm/valid-safe-number? value))
(u/not-valid plugin-id :leftPadding value)
@ -319,7 +326,27 @@
(u/not-valid plugin-id :leftPadding "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
(update-padding this id {:p4 value})))}
;; `:simple` mirrors vertical/horizontal padding; `:multiple` honours each side.
:paddingType
{:this true
:get #(-> % u/proxy->shape :layout-padding-type (d/nilv :simple) d/name)
:set
(fn [_ value]
(let [value (keyword value)]
(cond
(not (contains? ctl/padding-type value))
(u/not-valid plugin-id :paddingType value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :paddingType "Plugin doesn't have 'content:write' permission")
(not (u/page-active? page-id))
(u/not-valid plugin-id :paddingType "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding-type value})))))}
:addRow
(fn [type value]

View File

@ -56,6 +56,7 @@
[frontend-tests.ui.comments-clustering-test]
[frontend-tests.ui.comments-position-modifier-test]
[frontend-tests.ui.ds-controls-numeric-input-test]
[frontend-tests.ui.layout-container-multiple-test]
[frontend-tests.ui.measures-menu-props-test]
[frontend-tests.util-object-test]
[frontend-tests.util-range-tree-test]
@ -128,6 +129,7 @@
'frontend-tests.ui.comments-clustering-test
'frontend-tests.ui.comments-position-modifier-test
'frontend-tests.ui.ds-controls-numeric-input-test
'frontend-tests.ui.layout-container-multiple-test
'frontend-tests.ui.measures-menu-props-test
'frontend-tests.util-object-test
'frontend-tests.util-range-tree-test

View File

@ -0,0 +1,46 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.ui.layout-container-multiple-test
(:require
[app.main.ui.workspace.sidebar.options.shapes.multiple :as multiple]
[cljs.test :as t :include-macros true]))
(defn- flex-frame
[id padding-type padding]
{:id id
:type :frame
:layout :flex
:layout-flex-dir :row
:layout-gap-type :multiple
:layout-gap {:row-gap 0 :column-gap 0}
:layout-align-items :start
:layout-justify-content :start
:layout-align-content :stretch
:layout-wrap-type :nowrap
:layout-padding-type padding-type
:layout-padding padding})
(defn- layout-container-values
[shapes]
(let [[_ids values _tokens] (multiple/get-attrs* shapes {} :layout-container)]
values))
(t/deftest multiple-selection-padding-keeps-matching-sides
(let [values (layout-container-values
[(flex-frame :frame-1 :simple {:p1 10 :p2 20 :p3 10 :p4 20})
(flex-frame :frame-2 :simple {:p1 10 :p2 20 :p3 30 :p4 40})])]
(t/is (= {:p1 10 :p2 20 :p3 :multiple :p4 :multiple}
(:layout-padding values)))
(t/is (= :multiple (:layout-padding-type values)))))
(t/deftest multiple-selection-padding-type-does-not-demote
(let [values (layout-container-values
[(flex-frame :frame-1 :multiple {:p1 10 :p2 20 :p3 10 :p4 20})
(flex-frame :frame-2 :multiple {:p1 10 :p2 20 :p3 10 :p4 20})])]
(t/is (= {:p1 10 :p2 20 :p3 10 :p4 20}
(:layout-padding values)))
(t/is (= :multiple (:layout-padding-type values)))))

View File

@ -1,4 +1,14 @@
## 1.5.0 (Unreleased)
## 1.6.0 (Unreleased)
### 🚀 Features
- **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently.
### 🩹 Fixes
- **plugins-runtime**: Setting an individual padding/margin side (`leftPadding`, `topMargin`, …) now re-derives the padding/margin type, switching to `multiple` when the four sides stop being symmetric (so the value is actually painted) and back to `simple` once top/bottom and left/right are mirrored again.
## 1.5.0 (2026-07-08)
### 💣 Breaking changes & Deprecations

View File

@ -66,6 +66,7 @@
"justifyContent",
"justifyItems",
"leftPadding",
"paddingType",
"remove",
"rightPadding",
"rowGap",
@ -254,6 +255,7 @@
"horizontalMargin",
"horizontalSizing",
"leftMargin",
"marginType",
"maxHeight",
"maxWidth",
"minHeight",
@ -2032,6 +2034,12 @@
"type": null,
"array": false
},
"paddingType": {
"decl": "CommonLayout",
"kind": "getset",
"type": null,
"array": false
},
"horizontalSizing": {
"decl": "CommonLayout",
"kind": "getset",
@ -3242,6 +3250,12 @@
"type": null,
"array": false
},
"paddingType": {
"decl": "CommonLayout",
"kind": "getset",
"type": null,
"array": false
},
"horizontalSizing": {
"decl": "CommonLayout",
"kind": "getset",
@ -3590,6 +3604,12 @@
"type": null,
"array": false
},
"paddingType": {
"decl": "CommonLayout",
"kind": "getset",
"type": null,
"array": false
},
"horizontalSizing": {
"decl": "CommonLayout",
"kind": "getset",
@ -4868,6 +4888,12 @@
"type": null,
"array": false
},
"marginType": {
"decl": "LayoutChildProperties",
"kind": "getset",
"type": null,
"array": false
},
"maxWidth": {
"decl": "LayoutChildProperties",
"kind": "getset",

View File

@ -82,6 +82,34 @@ describe('Layout', () => {
expect(flex.leftPadding).toBeCloseTo(4.5, 2);
});
// paddingType is "simple" (sides mirrored) or "multiple" (each side independent).
test('paddingType round-trips', (ctx) => {
const flex = board(ctx).addFlexLayout();
expect(flex.paddingType).toBe('simple');
flex.paddingType = 'multiple';
expect(flex.paddingType).toBe('multiple');
flex.paddingType = 'simple';
expect(flex.paddingType).toBe('simple');
});
// Issue #10278: a single asymmetric side must switch paddingType to "multiple" so it paints.
test('setting an individual padding side switches paddingType to multiple', (ctx) => {
const flex = board(ctx).addFlexLayout();
expect(flex.paddingType).toBe('simple');
flex.leftPadding = 40;
expect(flex.leftPadding).toBeCloseTo(40, 0);
expect(flex.paddingType).toBe('multiple');
});
// Re-derived from the sides: once symmetric again, the type collapses to "simple".
test('padding collapsing back to symmetric restores simple type', (ctx) => {
const flex = board(ctx).addFlexLayout();
flex.leftPadding = 40;
expect(flex.paddingType).toBe('multiple');
flex.leftPadding = 0;
expect(flex.paddingType).toBe('simple');
});
test('sizing round-trips', (ctx) => {
const flex = board(ctx).addFlexLayout();
flex.horizontalSizing = 'fix';
@ -210,6 +238,22 @@ describe('Layout', () => {
expect(grid.leftPadding).toBeCloseTo(4.5, 2);
});
// paddingType behaves the same as on flex layouts (see issue #10278).
test('paddingType round-trips', (ctx) => {
const grid = board(ctx).addGridLayout();
expect(grid.paddingType).toBe('simple');
grid.paddingType = 'multiple';
expect(grid.paddingType).toBe('multiple');
});
test('setting an individual padding side switches paddingType to multiple', (ctx) => {
const grid = board(ctx).addGridLayout();
expect(grid.paddingType).toBe('simple');
grid.leftPadding = 40;
expect(grid.leftPadding).toBeCloseTo(40, 0);
expect(grid.paddingType).toBe('multiple');
});
// Index boundaries — invalid indices must be rejected.
test('addRowAtIndex with a negative index throws', (ctx) => {
const grid = board(ctx).addGridLayout();
@ -389,6 +433,55 @@ describe('Layout', () => {
}
});
// marginType is the child-margin counterpart of a layout's paddingType.
test('marginType round-trips', (ctx) => {
const b = board(ctx);
const flex = b.addFlexLayout();
const rect = ctx.penpot.createRectangle();
flex.appendChild(rect);
const child = rect.layoutChild;
expect(child).toBeDefined();
if (child) {
expect(child.marginType).toBe('simple');
child.marginType = 'multiple';
expect(child.marginType).toBe('multiple');
child.marginType = 'simple';
expect(child.marginType).toBe('simple');
}
});
// Issue #10278 (margins): a single asymmetric side must switch marginType to "multiple".
test('setting an individual margin side switches marginType to multiple', (ctx) => {
const b = board(ctx);
const flex = b.addFlexLayout();
const rect = ctx.penpot.createRectangle();
flex.appendChild(rect);
const child = rect.layoutChild;
expect(child).toBeDefined();
if (child) {
expect(child.marginType).toBe('simple');
child.leftMargin = 12;
expect(child.leftMargin).toBeCloseTo(12, 0);
expect(child.marginType).toBe('multiple');
}
});
// Symmetric margins collapse the child back to "simple", mirroring padding.
test('margin collapsing back to symmetric restores simple type', (ctx) => {
const b = board(ctx);
const flex = b.addFlexLayout();
const rect = ctx.penpot.createRectangle();
flex.appendChild(rect);
const child = rect.layoutChild;
expect(child).toBeDefined();
if (child) {
child.leftMargin = 12;
expect(child.marginType).toBe('multiple');
child.leftMargin = 0;
expect(child.marginType).toBe('simple');
}
});
// Community report (forum #10700, issue #12): layoutChild was said to be
// null for children appended to a flex board that is re-found (fresh
// proxy) instead of using the creation-time reference. Did not reproduce;

View File

@ -747,6 +747,13 @@ export interface CommonLayout {
* The `leftPadding` property specifies the padding at the left of the container.
*/
leftPadding: number;
/**
* The `paddingType` property specifies how the four padding values are applied.
* It can be one of the following values:
* - 'simple': the vertical and horizontal paddings are mirrored across both sides.
* - 'multiple': each of the four sides (top, right, bottom, left) is honoured independently.
*/
paddingType: 'simple' | 'multiple';
/**
* The `horizontalSizing` property specifies the horizontal sizing behavior of the container.
@ -2576,6 +2583,14 @@ export interface LayoutChildProperties {
*/
leftMargin: number;
/**
* The `marginType` property specifies how the four margin values are applied.
* It can be one of the following values:
* - 'simple': the vertical and horizontal margins are mirrored across both sides.
* - 'multiple': each of the four sides (top, right, bottom, left) is honoured independently.
*/
marginType: 'simple' | 'multiple';
/**
* Defines the maximum width of the child element.
* If set to null, there is no maximum width constraint.