From 242ee4845bed99947c5dc141138cbe119739c945 Mon Sep 17 00:00:00 2001 From: Luis Ibarra Date: Mon, 16 Mar 2026 20:47:13 -0500 Subject: [PATCH 1/5] feat(widget): add content padding functionality for container-like widgets - Introduced `DEFAULT_CONTENT_PADDING` constant for default inner content padding. - Implemented `parseContentPadding` function to handle various padding input formats and return appropriate values. - Updated `ContainerWidget` and `FormWidget` to utilize the new content padding feature. - Enhanced tests for `parseContentPadding` to ensure correct parsing and default behavior. This change improves the flexibility of widget styling by allowing customizable padding options. --- app/client/src/constants/WidgetConstants.tsx | 3 ++ .../ContainerWidget/component/index.tsx | 16 +++++- .../widgets/ContainerWidget/widget/index.tsx | 37 +++++++++++--- .../src/widgets/FormWidget/widget/index.tsx | 7 ++- app/client/src/widgets/WidgetUtils.test.ts | 31 +++++++++++ app/client/src/widgets/WidgetUtils.ts | 51 +++++++++++++++++++ 6 files changed, 135 insertions(+), 10 deletions(-) diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index d5777458d5f7..078434aad6ae 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -110,6 +110,9 @@ export const AUTO_LAYOUT_CONTAINER_PADDING = 5; export const WIDGET_PADDING = GridDefaults.DEFAULT_GRID_ROW_HEIGHT * 0.4; +/** Default inner content padding (px) for container-like widgets when contentPadding is not set. */ +export const DEFAULT_CONTENT_PADDING = "4"; + export const WIDGET_CLASSNAME_PREFIX = "WIDGET_"; export const MAIN_CONTAINER_WIDGET_ID = "0"; export const MAIN_CONTAINER_WIDGET_NAME = "MainContainer"; diff --git a/app/client/src/widgets/ContainerWidget/component/index.tsx b/app/client/src/widgets/ContainerWidget/component/index.tsx index 43ad5c133760..4c768645ffad 100644 --- a/app/client/src/widgets/ContainerWidget/component/index.tsx +++ b/app/client/src/widgets/ContainerWidget/component/index.tsx @@ -11,7 +11,7 @@ import fastdom from "fastdom"; import { generateClassName, getCanvasClassName } from "utils/generators"; import type { WidgetStyleContainerProps } from "components/designSystems/appsmith/WidgetStyleContainer"; import WidgetStyleContainer from "components/designSystems/appsmith/WidgetStyleContainer"; -import { scrollCSS } from "widgets/WidgetUtils"; +import { parseContentPadding, scrollCSS } from "widgets/WidgetUtils"; import { useSelector } from "react-redux"; import { LayoutSystemTypes } from "layoutSystems/types"; import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; @@ -32,6 +32,13 @@ const StyledContainerComponent = styled.div< opacity: ${(props) => props.resizeDisabled && !props.forceFullOpacity ? "0.8" : "1"}; + padding: ${(props) => { + const [t, r, b, l] = props.contentPaddingPx; + + return `${t}px ${r}px ${b}px ${l}px`; + }}; + box-sizing: border-box; + background: ${(props) => props.backgroundColor}; &:hover { background-color: ${(props) => { @@ -55,6 +62,7 @@ interface ContainerWrapperProps { dropDisabled?: boolean; $noScroll: boolean; forceFullOpacity?: boolean; + contentPaddingPx: [number, number, number, number]; } function ContainerComponentWrapper( @@ -134,6 +142,7 @@ function ContainerComponentWrapper( ? "auto-layout" : "" }`} + contentPaddingPx={props.contentPaddingPx} dropDisabled={props.dropDisabled} forceFullOpacity={props.forceFullOpacity} onClick={props.onClick} @@ -151,10 +160,13 @@ function ContainerComponentWrapper( } function ContainerComponent(props: ContainerComponentProps) { + const contentPaddingPx = parseContentPadding(props.contentPadding ?? ""); + if (props.detachFromLayout) { return ( { - return map( - // sort by row so stacking context is correct - // TODO(abhinav): This is hacky. The stacking context should increase for widgets rendered top to bottom, always. - // Figure out a way in which the stacking context is consistent. + const children = this.props.positioning !== Positioning.Fixed ? this.props.children - : sortBy(compact(this.props.children), (child) => child.topRow), - this.renderChildWidget, - ); + : sortBy(compact(this.props.children), (child) => child.topRow); + + return map(children, (child) => ( + + {this.renderChildWidget(child)} + + )); }; renderAsContainerComponent(props: ContainerWidgetProps) { @@ -411,6 +431,7 @@ export interface ContainerWidgetProps extends WidgetProps { children?: T[]; containerStyle?: ContainerStyle; + contentPadding?: string; onClick?: MouseEventHandler; onClickCapture?: MouseEventHandler; shouldScrollContents?: boolean; diff --git a/app/client/src/widgets/FormWidget/widget/index.tsx b/app/client/src/widgets/FormWidget/widget/index.tsx index 94a6116a948c..d2a059b5d0b2 100644 --- a/app/client/src/widgets/FormWidget/widget/index.tsx +++ b/app/client/src/widgets/FormWidget/widget/index.tsx @@ -22,7 +22,11 @@ import type { SetterConfig } from "entities/AppTheming"; import { ButtonVariantTypes, RecaptchaTypes } from "components/constants"; import { Colors } from "constants/Colors"; import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants"; -import { GridDefaults, WIDGET_TAGS } from "constants/WidgetConstants"; +import { + DEFAULT_CONTENT_PADDING, + GridDefaults, + WIDGET_TAGS, +} from "constants/WidgetConstants"; import type { CanvasWidgetsReduxState } from "ee/reducers/entityReducers/canvasWidgetsReducer"; import { getWidgetBluePrintUpdates } from "utils/WidgetBlueprintUtils"; import { DynamicHeight } from "utils/WidgetFeatures"; @@ -82,6 +86,7 @@ class FormWidget extends ContainerWidget { columns: 24, borderColor: Colors.GREY_5, borderWidth: "1", + contentPadding: DEFAULT_CONTENT_PADDING, animateLoading: true, widgetName: "Form", backgroundColor: Colors.WHITE, diff --git a/app/client/src/widgets/WidgetUtils.test.ts b/app/client/src/widgets/WidgetUtils.test.ts index cc11b088b06c..ce0a1310a356 100644 --- a/app/client/src/widgets/WidgetUtils.test.ts +++ b/app/client/src/widgets/WidgetUtils.test.ts @@ -27,6 +27,7 @@ import { getWidgetMaxAutoHeight, getWidgetMinAutoHeight, isCompactMode, + parseContentPadding, } from "./WidgetUtils"; import { getCustomTextColor, @@ -760,3 +761,33 @@ describe("Should Update Widget Height Automatically?", () => { expect(isCompactMode(unCompactHeight)).toBeFalsy(); }); }); + +describe("parseContentPadding", () => { + it("parses 1 value to all sides", () => { + expect(parseContentPadding("10")).toEqual([10, 10, 10, 10]); + }); + it("parses 2 values to vertical, horizontal", () => { + expect(parseContentPadding("10 20")).toEqual([10, 20, 10, 20]); + }); + it("parses 3 values to top, left-right, bottom", () => { + expect(parseContentPadding("10 20 30")).toEqual([10, 20, 30, 20]); + }); + it("parses 4 values to top, right, bottom, left", () => { + expect(parseContentPadding("10 20 30 40")).toEqual([10, 20, 30, 40]); + }); + it("returns default for invalid input", () => { + expect(parseContentPadding("abc")).toEqual([4, 4, 4, 4]); + expect(parseContentPadding("-1")).toEqual([4, 4, 4, 4]); + expect(parseContentPadding("1 2 3 4 5")).toEqual([4, 4, 4, 4]); + }); + it("returns default for empty string", () => { + expect(parseContentPadding("")).toEqual([4, 4, 4, 4]); + expect(parseContentPadding(" ")).toEqual([4, 4, 4, 4]); + }); + it("uses custom fallback when provided", () => { + const fb: [number, number, number, number] = [0, 0, 0, 0]; + + expect(parseContentPadding("", fb)).toEqual([0, 0, 0, 0]); + expect(parseContentPadding("invalid", fb)).toEqual([0, 0, 0, 0]); + }); +}); diff --git a/app/client/src/widgets/WidgetUtils.ts b/app/client/src/widgets/WidgetUtils.ts index 933435896fc4..b8a43681c5cf 100644 --- a/app/client/src/widgets/WidgetUtils.ts +++ b/app/client/src/widgets/WidgetUtils.ts @@ -15,6 +15,7 @@ import type { PropertyUpdates } from "WidgetProvider/types"; import { CANVAS_SELECTOR, CONTAINER_GRID_PADDING, + DEFAULT_CONTENT_PADDING, GridDefaults, TextSizes, WidgetHeightLimits, @@ -74,6 +75,56 @@ export function getSnapSpaces(props: WidgetPositionProps) { }; } +/** + * Parse a padding string (1–4 space-separated numbers, CSS shorthand) into [top, right, bottom, left] in px. + * 1 value → all sides; 2 → vertical, horizontal; 3 → top, left-right, bottom; 4 → top, right, bottom, left. + * Invalid or empty input returns fallback or default from DEFAULT_CONTENT_PADDING. + */ +export function parseContentPadding( + paddingStr: string | number | undefined | null, + fallback?: [number, number, number, number], +): [number, number, number, number] { + const raw = + paddingStr != null && typeof paddingStr === "string" + ? paddingStr + : paddingStr != null + ? String(paddingStr) + : ""; + const str = raw.trim(); + + if (!str) { + const d = parseInt(DEFAULT_CONTENT_PADDING, 10) || 0; + + return fallback ?? [d, d, d, d]; + } + + const tokens = str.split(/\s+/).map((t) => parseFloat(t)); + const valid = tokens.every((n) => !Number.isNaN(n) && n >= 0); + + if (!valid || tokens.length < 1 || tokens.length > 4) { + const d = parseInt(DEFAULT_CONTENT_PADDING, 10) || 0; + + return fallback ?? [d, d, d, d]; + } + + const [a, b, c, d] = tokens; + + switch (tokens.length) { + case 1: + return [a, a, a, a]; + case 2: + return [a, b, a, b]; + case 3: + return [a, b, c, b]; + case 4: + return [a, b, c, d]; + default: + const def = parseInt(DEFAULT_CONTENT_PADDING, 10) || 0; + + return fallback ?? [def, def, def, def]; + } +} + export const DefaultAutocompleteDefinitions = { isVisible: { "!type": "bool", From e68ca3aa2832ce7beb55768ffb42d53c0da545a4 Mon Sep 17 00:00:00 2001 From: Luis Ibarra Date: Mon, 16 Mar 2026 20:57:57 -0500 Subject: [PATCH 2/5] feat(widget): implement contentPaddingValidation for flexible padding options - Added `contentPaddingValidation` function to validate and parse content padding inputs for container-like widgets. - Updated `ContainerWidget` to utilize the new validation function for content padding properties. - Enhanced unit tests for `contentPaddingValidation` to cover various input scenarios, ensuring robust validation logic. This change improves the user experience by allowing more flexible and error-resistant padding configurations. --- .../widgets/ContainerWidget/widget/index.tsx | 15 ++- app/client/src/widgets/WidgetUtils.test.ts | 121 ++++++++++++++++++ app/client/src/widgets/WidgetUtils.ts | 62 +++++++++ 3 files changed, 195 insertions(+), 3 deletions(-) diff --git a/app/client/src/widgets/ContainerWidget/widget/index.tsx b/app/client/src/widgets/ContainerWidget/widget/index.tsx index 5373d84acdd6..b510f6d4e94c 100644 --- a/app/client/src/widgets/ContainerWidget/widget/index.tsx +++ b/app/client/src/widgets/ContainerWidget/widget/index.tsx @@ -5,6 +5,7 @@ import type { ContainerStyle } from "../component"; import ContainerComponent from "../component"; import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; +import type { ValidationConfig } from "constants/PropertyControlConstants"; import { ValidationTypes } from "constants/WidgetValidation"; import { compact, get, map, sortBy } from "lodash"; import WidgetsMultiSelectBox from "layoutSystems/fixedlayout/common/widgetGrouping/WidgetsMultiSelectBox"; @@ -12,6 +13,7 @@ import type { SetterConfig, Stylesheet } from "entities/AppTheming"; import { getSnappedGrid } from "sagas/WidgetOperationUtils"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { + contentPaddingValidation, isAutoHeightEnabledForWidget, DefaultAutocompleteDefinitions, isAutoHeightEnabledForWidgetWithLimits, @@ -305,9 +307,16 @@ export class ContainerWidget extends BaseWidget< isBindProperty: true, isTriggerProperty: false, validation: { - type: ValidationTypes.NUMBER, - params: { min: 0 }, - }, + type: ValidationTypes.FUNCTION, + params: { + fn: contentPaddingValidation, + default: DEFAULT_CONTENT_PADDING, + expected: { + type: "1–4 space-separated numbers (px)", + example: "10 or 10 20 10 20", + }, + }, + } as ValidationConfig, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/WidgetUtils.test.ts b/app/client/src/widgets/WidgetUtils.test.ts index ce0a1310a356..43e1eabcac15 100644 --- a/app/client/src/widgets/WidgetUtils.test.ts +++ b/app/client/src/widgets/WidgetUtils.test.ts @@ -28,6 +28,7 @@ import { getWidgetMinAutoHeight, isCompactMode, parseContentPadding, + contentPaddingValidation, } from "./WidgetUtils"; import { getCustomTextColor, @@ -791,3 +792,123 @@ describe("parseContentPadding", () => { expect(parseContentPadding("invalid", fb)).toEqual([0, 0, 0, 0]); }); }); + +describe("contentPaddingValidation", () => { + const noop = {} as Record; + const defaultConfig = { params: { default: "4" } }; + + it("accepts 1 value", () => { + const r = contentPaddingValidation( + "10", + noop, + null, + null, + "", + defaultConfig, + ); + + expect(r.isValid).toBe(true); + expect(r.parsed).toBe("10"); + }); + + it("accepts 2 values", () => { + const r = contentPaddingValidation( + "10 20", + noop, + null, + null, + "", + defaultConfig, + ); + + expect(r.isValid).toBe(true); + expect(r.parsed).toBe("10 20"); + }); + + it("accepts 3 values", () => { + const r = contentPaddingValidation( + "10 20 30", + noop, + null, + null, + "", + defaultConfig, + ); + + expect(r.isValid).toBe(true); + expect(r.parsed).toBe("10 20 30"); + }); + + it("accepts 4 values", () => { + const r = contentPaddingValidation( + "10 20 10 20", + noop, + null, + null, + "", + defaultConfig, + ); + + expect(r.isValid).toBe(true); + expect(r.parsed).toBe("10 20 10 20"); + }); + + it("returns valid with default for empty string", () => { + const r = contentPaddingValidation("", noop, null, null, "", defaultConfig); + + expect(r.isValid).toBe(true); + expect(r.parsed).toBe("4"); + }); + + it("rejects more than 4 tokens", () => { + const r = contentPaddingValidation( + "10 20 30 40 50", + noop, + null, + null, + "", + defaultConfig, + ); + + expect(r.isValid).toBe(false); + expect(r.parsed).toBe("4"); + expect(r.messages?.length).toBeGreaterThan(0); + }); + + it("rejects non-numeric token", () => { + const r = contentPaddingValidation( + "10 abc 20", + noop, + null, + null, + "", + defaultConfig, + ); + + expect(r.isValid).toBe(false); + expect(r.parsed).toBe("4"); + }); + + it("rejects negative value", () => { + const r = contentPaddingValidation( + "10 -5 20", + noop, + null, + null, + "", + defaultConfig, + ); + + expect(r.isValid).toBe(false); + expect(r.parsed).toBe("4"); + }); + + it("uses config default when empty", () => { + const r = contentPaddingValidation("", noop, null, null, "", { + params: { default: "25" }, + }); + + expect(r.isValid).toBe(true); + expect(r.parsed).toBe("25"); + }); +}); diff --git a/app/client/src/widgets/WidgetUtils.ts b/app/client/src/widgets/WidgetUtils.ts index b8a43681c5cf..2b80cab59b02 100644 --- a/app/client/src/widgets/WidgetUtils.ts +++ b/app/client/src/widgets/WidgetUtils.ts @@ -125,6 +125,68 @@ export function parseContentPadding( } } +/** + * Validation for contentPadding property: 1–4 space-separated numbers (px), each >= 0. + * Rejects empty tokens, non-numeric, negative, or wrong token count. + * Used by Container and Form (and other container-like widgets) in property pane. + */ +export function contentPaddingValidation( + value: unknown, + _props?: Record, + _lodash?: unknown, + _moment?: unknown, + _propertyPath?: string, + config?: { params?: { default?: string } }, +): { + isValid: boolean; + parsed: string; + messages?: Array<{ name: string; message: string }>; +} { + const defaultPadding = config?.params?.default ?? "4"; + const str = + value == null ? "" : typeof value === "string" ? value : String(value); + const trimmed = str.trim(); + + if (trimmed === "") { + return { isValid: true, parsed: defaultPadding }; + } + + const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0); + + if (tokens.length < 1 || tokens.length > 4) { + return { + isValid: false, + parsed: defaultPadding, + messages: [ + { + name: "ValidationError", + message: + "Enter 1 to 4 space-separated numbers (e.g. 10 or 10 20 10 20)", + }, + ], + }; + } + + for (let i = 0; i < tokens.length; i++) { + const n = parseFloat(tokens[i]); + + if (Number.isNaN(n) || n < 0) { + return { + isValid: false, + parsed: defaultPadding, + messages: [ + { + name: "ValidationError", + message: "Each value must be a non-negative number", + }, + ], + }; + } + } + + return { isValid: true, parsed: trimmed }; +} + export const DefaultAutocompleteDefinitions = { isVisible: { "!type": "bool", From 66348bc352509bd5024fc707c0d66e21e1de1b8c Mon Sep 17 00:00:00 2001 From: Luis Ibarra Date: Mon, 16 Mar 2026 21:19:57 -0500 Subject: [PATCH 3/5] feat(widget): enhance content padding support across multiple widgets - Introduced `contentPadding` property to `JSONFormWidget`, `ListWidgetV2`, and `TabsWidget` for customizable inner padding. - Updated relevant components to utilize the new padding feature, allowing for flexible styling options. - Implemented validation for `contentPadding` input to ensure correct formatting and usability. - Enhanced documentation and property configuration for better user guidance on padding settings. This change improves the visual consistency and user experience by allowing developers to specify padding for various widgets. --- .../widgets/JSONFormWidget/component/Form.tsx | 29 +++++++++----- .../JSONFormWidget/component/index.tsx | 2 + .../widgets/JSONFormWidget/widget/index.tsx | 7 ++++ .../JSONFormWidget/widget/propertyConfig.ts | 23 +++++++++++ .../widgets/ListWidgetV2/component/index.tsx | 39 +++++++++++++++++-- .../ListWidgetV2/widget/defaultProps.ts | 6 ++- .../src/widgets/ListWidgetV2/widget/index.tsx | 16 +++++++- .../ListWidgetV2/widget/propertyConfig.ts | 24 ++++++++++++ .../widgets/TabsWidget/component/index.tsx | 12 +++++- .../src/widgets/TabsWidget/constants.ts | 1 + .../src/widgets/TabsWidget/widget/index.tsx | 31 +++++++++++++++ 11 files changed, 173 insertions(+), 17 deletions(-) diff --git a/app/client/src/widgets/JSONFormWidget/component/Form.tsx b/app/client/src/widgets/JSONFormWidget/component/Form.tsx index 353a0f528d77..157bbd3761f5 100644 --- a/app/client/src/widgets/JSONFormWidget/component/Form.tsx +++ b/app/client/src/widgets/JSONFormWidget/component/Form.tsx @@ -10,16 +10,20 @@ import useFixedFooter from "./useFixedFooter"; import type { ButtonStyleProps } from "widgets/ButtonWidget/component"; import { BaseButton as Button } from "widgets/ButtonWidget/component"; import { Colors } from "constants/Colors"; -import { FORM_PADDING_Y, FORM_PADDING_X } from "./styleConstants"; import type { Schema } from "../constants"; import { ROOT_SCHEMA_KEY } from "../constants"; import { convertSchemaItemToFormData, schemaItemDefaultValue } from "../helper"; import { klonaRegularWithTelemetry } from "utils/helpers"; +const DEFAULT_FORM_PADDING_PX: [number, number, number, number] = [ + 25, 25, 25, 25, +]; + // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any export type FormProps = PropsWithChildren<{ backgroundColor?: string; + contentPaddingPx?: [number, number, number, number]; disabledWhenInvalid?: boolean; fixedFooter: boolean; getFormData: () => TValues; @@ -47,20 +51,21 @@ interface StyledFormProps { scrollContents: boolean; } -interface StyledFormBodyProps { - stretchBodyVertically: boolean; -} - interface StyledFooterProps { fixedFooter: boolean; backgroundColor?: string; + $contentPaddingPx: [number, number, number, number]; +} + +interface StyledFormBodyPropsWithPadding { + stretchBodyVertically: boolean; + $contentPaddingPx: [number, number, number, number]; } const BUTTON_WIDTH = 110; const FOOTER_BUTTON_GAP = 10; const TITLE_FONT_SIZE = "1.25rem"; const FOOTER_DEFAULT_BG_COLOR = "#fff"; -const FOOTER_PADDING_TOP = FORM_PADDING_Y; const TITLE_MARGIN_BOTTOM = 16; const FOOTER_SCROLL_ACTIVE_CLASS_NAME = "scroll-active"; @@ -71,8 +76,8 @@ const StyledFormFooter = styled.div` display: flex; gap: ${FOOTER_BUTTON_GAP}px; justify-content: flex-end; - padding: ${FORM_PADDING_Y}px ${FORM_PADDING_X}px; - padding-top: ${FOOTER_PADDING_TOP}px; + padding: ${({ $contentPaddingPx: p }) => + `${p[0]}px ${p[1]}px ${p[2]}px ${p[3]}px`}; position: ${({ fixedFooter }) => fixedFooter && "sticky"}; width: 100%; @@ -104,10 +109,11 @@ const StyledTitle = styled(Text)<{ margin-bottom: ${TITLE_MARGIN_BOTTOM}px; `; -const StyledFormBody = styled.div` +const StyledFormBody = styled.div` height: ${({ stretchBodyVertically }) => stretchBodyVertically ? "100%" : "auto"}; - padding: ${FORM_PADDING_Y}px ${FORM_PADDING_X}px; + padding: ${({ $contentPaddingPx: p }) => + `${p[0]}px ${p[1]}px ${p[2]}px ${p[3]}px`}; `; const StyledResetButtonWrapper = styled.div``; @@ -124,6 +130,7 @@ function Form( { backgroundColor, children, + contentPaddingPx, disabledWhenInvalid, fixedFooter, getFormData, @@ -281,6 +288,7 @@ function Form( scrollContents={scrollContents} > @@ -289,6 +297,7 @@ function Form( {!hideFooter && ( { borderWidth?: number; boxShadow?: BoxShadow; boxShadowColor?: string; + contentPaddingPx?: [number, number, number, number]; disabledWhenInvalid?: boolean; executeAction: (action: Action) => void; fieldLimitExceeded: boolean; @@ -213,6 +214,7 @@ function JSONFormComponent(
; + contentPadding?: string; disabledWhenInvalid?: boolean; fieldLimitExceeded: boolean; fieldState: Record; @@ -196,6 +198,7 @@ class JSONFormWidget extends BaseWidget< version: 1, borderWidth: "1", borderColor: Colors.GREY_5, + contentPadding: "25", widgetName: "JSONForm", autoGenerateForm: true, fieldLimitExceeded: false, @@ -830,6 +833,9 @@ class JSONFormWidget extends BaseWidget< getWidgetView() { const isAutoHeightEnabled = isAutoHeightEnabledForWidget(this.props); + const contentPaddingPx = parseContentPadding( + this.props.contentPadding ?? "25", + ); return ( // Warning!!! Do not ever introduce formData as a prop directly, @@ -842,6 +848,7 @@ class JSONFormWidget extends BaseWidget< borderWidth={this.props.borderWidth} boxShadow={this.props.boxShadow} boxShadowColor={this.props.boxShadowColor} + contentPaddingPx={contentPaddingPx} disabledWhenInvalid={this.props.disabledWhenInvalid} executeAction={this.onExecuteAction} fieldLimitExceeded={this.props.fieldLimitExceeded} diff --git a/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts b/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts index d4e4d47d873d..ad2711277b50 100644 --- a/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts +++ b/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts @@ -16,6 +16,8 @@ import { SUCCESSFULL_BINDING_MESSAGE, } from "../constants/messages"; import { createMessage } from "ee/constants/messages"; +import type { ValidationConfig } from "constants/PropertyControlConstants"; +import { contentPaddingValidation } from "widgets/WidgetUtils"; import { FieldOptionsType } from "components/editorComponents/WidgetQueryGeneratorForm/WidgetSpecificControls/OtherFields/Field/Dropdown/types"; import { DROPDOWN_VARIANT } from "components/editorComponents/WidgetQueryGeneratorForm/CommonControls/DatasourceDropdown/types"; @@ -671,6 +673,27 @@ export const styleConfig = [ isTriggerProperty: false, validation: { type: ValidationTypes.NUMBER }, }, + { + helpText: + "Inner padding (px). Use one value for all sides or 2–4 values (e.g. 10 20 10 20 for top right bottom left)", + propertyName: "contentPadding", + label: "Padding (px)", + placeholderText: "e.g. 10 or 10 20 10 20", + controlType: "INPUT_TEXT", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: contentPaddingValidation, + default: "25", + expected: { + type: "1–4 space-separated numbers (px)", + example: "10 or 10 20 10 20", + }, + }, + } as ValidationConfig, + }, { propertyName: "borderRadius", helpText: "Enter value for border radius", diff --git a/app/client/src/widgets/ListWidgetV2/component/index.tsx b/app/client/src/widgets/ListWidgetV2/component/index.tsx index 507d3703a696..12448eccf2cb 100644 --- a/app/client/src/widgets/ListWidgetV2/component/index.tsx +++ b/app/client/src/widgets/ListWidgetV2/component/index.tsx @@ -10,6 +10,7 @@ type ListComponentProps = React.PropsWithChildren<{ borderRadius: string; boxShadow?: string; componentRef: RefObject; + contentPaddingPx?: [number, number, number, number]; height: number; infiniteScroll?: boolean; }>; @@ -47,11 +48,39 @@ export const ListComponentEmpty = styled.div<{ `; // This is to be improved for infiniteScroll. +// Vertical padding is applied as margin so the scroll height stays correct and +// content is not clipped; horizontal padding is applied as padding. +// When padding is set, the wrapper height is reduced by top+bottom so that +// marginTop + height + marginBottom fits in the list container. const ScrollableCanvasWrapper = styled.div< - Pick + Pick & { + $contentPaddingPx?: [number, number, number, number]; + } >` + box-sizing: border-box; ${({ infiniteScroll }) => (infiniteScroll ? scrollCSS : ``)} - height: ${(props) => props.height - WIDGET_PADDING * 2}px; + height: ${(props) => { + const base = props.height - WIDGET_PADDING * 2; + const p = props.$contentPaddingPx; + + if (!p) return `${base}px`; + + return `${base - p[0] - p[2]}px`; + }}; + ${(props) => { + const p = props.$contentPaddingPx; + + if (!p) return ""; + + const [top, right, bottom, left] = p; + + return ` + margin-top: ${top}px; + margin-bottom: ${bottom}px; + padding-left: ${left}px; + padding-right: ${right}px; + `; + }} `; function ListComponent(props: ListComponentProps) { @@ -71,7 +100,11 @@ function ListComponent(props: ListComponentProps) { boxShadow={boxShadow} ref={componentRef} > - + {props.children} diff --git a/app/client/src/widgets/ListWidgetV2/widget/defaultProps.ts b/app/client/src/widgets/ListWidgetV2/widget/defaultProps.ts index ee735b07d311..570132ab0e82 100644 --- a/app/client/src/widgets/ListWidgetV2/widget/defaultProps.ts +++ b/app/client/src/widgets/ListWidgetV2/widget/defaultProps.ts @@ -9,7 +9,10 @@ import { } from "./helper"; import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants"; import { getWidgetBluePrintUpdates } from "utils/WidgetBlueprintUtils"; -import { GridDefaults } from "constants/WidgetConstants"; +import { + DEFAULT_CONTENT_PADDING, + GridDefaults, +} from "constants/WidgetConstants"; import type { CanvasWidgetsReduxState } from "ee/reducers/entityReducers/canvasWidgetsReducer"; import { FlexLayerAlignment, @@ -45,6 +48,7 @@ const LIST_WIDGET_NESTING_ERROR = export default { backgroundColor: "transparent", + contentPadding: DEFAULT_CONTENT_PADDING, itemBackgroundColor: "#FFFFFF", requiresFlatWidgetChildren: true, hasMetaWidgets: true, diff --git a/app/client/src/widgets/ListWidgetV2/widget/index.tsx b/app/client/src/widgets/ListWidgetV2/widget/index.tsx index f14fb87f16ec..fb46fd0859aa 100644 --- a/app/client/src/widgets/ListWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/ListWidgetV2/widget/index.tsx @@ -31,6 +31,7 @@ import { PropertyPaneStyleConfig, } from "./propertyConfig"; import { + DEFAULT_CONTENT_PADDING, RenderModes, WIDGET_PADDING, WIDGET_TAGS, @@ -48,7 +49,10 @@ import { isListFullyEmpty, isTargetElementClickable, } from "./helper"; -import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; +import { + DefaultAutocompleteDefinitions, + parseContentPadding, +} from "widgets/WidgetUtils"; import type { ExtraDef } from "utils/autocomplete/defCreatorUtils"; import { LayoutSystemTypes } from "layoutSystems/types"; import { generateTypeDef } from "utils/autocomplete/defCreatorUtils"; @@ -1489,12 +1493,19 @@ class ListWidget extends BaseWidget< ); } + const contentPaddingPx = parseContentPadding( + this.props.contentPadding ?? DEFAULT_CONTENT_PADDING, + ); + const [, rightPx, , leftPx] = contentPaddingPx; + const paddedContentWidth = Math.max(0, componentWidth - leftPx - rightPx); + return ( @@ -1505,7 +1516,7 @@ class ListWidget extends BaseWidget< updateWidgetProperty={this.overrideUpdateWidgetProperty} > {this.renderChildren(this.props.metaWidgetChildrenStructure, { - componentWidth, + componentWidth: paddedContentWidth, parentColumnSpace, selectedItemKey, startIndex, @@ -1523,6 +1534,7 @@ export interface ListWidgetProps backgroundColor: string; borderRadius: string; boxShadow?: string; + contentPadding?: string; children?: T[]; currentItemStructure?: Record; itemSpacing?: number; diff --git a/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts b/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts index e82c72ab76ae..cca36110f8c4 100644 --- a/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts +++ b/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts @@ -13,6 +13,9 @@ import { LIST_WIDGET_V2_TOTAL_RECORD_TOOLTIP, createMessage, } from "ee/constants/messages"; +import type { ValidationConfig } from "constants/PropertyControlConstants"; +import { DEFAULT_CONTENT_PADDING } from "constants/WidgetConstants"; +import { contentPaddingValidation } from "widgets/WidgetUtils"; const MIN_ITEM_SPACING = 0; const MAX_ITEM_SPACING = 16; @@ -474,6 +477,27 @@ export const PropertyPaneStyleConfig = [ { sectionName: "Border and shadow", children: [ + { + helpText: + "Inner padding (px). Use one value for all sides or 2–4 values (e.g. 10 20 10 20 for top right bottom left). Applies to the list container only.", + propertyName: "contentPadding", + label: "Padding (px)", + placeholderText: "e.g. 10 or 10 20 10 20", + controlType: "INPUT_TEXT", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: contentPaddingValidation, + default: DEFAULT_CONTENT_PADDING, + expected: { + type: "1–4 space-separated numbers (px)", + example: "10 or 10 20 10 20", + }, + }, + } as ValidationConfig, + }, { propertyName: "borderRadius", label: "Border radius", diff --git a/app/client/src/widgets/TabsWidget/component/index.tsx b/app/client/src/widgets/TabsWidget/component/index.tsx index 0587b33327e1..638ccfd9325f 100644 --- a/app/client/src/widgets/TabsWidget/component/index.tsx +++ b/app/client/src/widgets/TabsWidget/component/index.tsx @@ -21,6 +21,7 @@ interface TabsComponentProps extends ComponentProps { borderColor?: string; accentColor?: string; primaryColor: string; + contentPaddingPx?: [number, number, number, number]; onTabChange: (tabId: string) => void; tabs: Array<{ id: string; @@ -110,10 +111,18 @@ export interface ScrollNavControlProps { className?: string; } -const ScrollCanvas = styled.div<{ $shouldScrollContents: boolean }>` +const ScrollCanvas = styled.div<{ + $shouldScrollContents: boolean; + $contentPaddingPx?: [number, number, number, number]; +}>` overflow: hidden; + box-sizing: border-box; ${(props) => (props.$shouldScrollContents ? scrollCSS : ``)} width: 100%; + ${(props) => + props.$contentPaddingPx + ? `padding: ${props.$contentPaddingPx[0]}px ${props.$contentPaddingPx[1]}px ${props.$contentPaddingPx[2]}px ${props.$contentPaddingPx[3]}px;` + : ""} `; function TabsComponent(props: TabsComponentProps) { @@ -199,6 +208,7 @@ function TabsComponent(props: TabsComponentProps) { )} borderRadius: string; boxShadow?: string; primaryColor: string; + contentPadding?: string; } export const SCROLL_NAV_CONTROL_CONTAINER_WIDTH = 30; diff --git a/app/client/src/widgets/TabsWidget/widget/index.tsx b/app/client/src/widgets/TabsWidget/widget/index.tsx index 2ae2afb32fe3..1eb83c4908b1 100644 --- a/app/client/src/widgets/TabsWidget/widget/index.tsx +++ b/app/client/src/widgets/TabsWidget/widget/index.tsx @@ -20,10 +20,13 @@ import type { TabContainerWidgetProps, TabsWidgetProps } from "../constants"; import derivedProperties from "./parseDerivedProperties"; import type { SetterConfig, Stylesheet } from "entities/AppTheming"; import { + contentPaddingValidation, isAutoHeightEnabledForWidget, isAutoHeightEnabledForWidgetWithLimits, DefaultAutocompleteDefinitions, + parseContentPadding, } from "widgets/WidgetUtils"; +import type { ValidationConfig } from "constants/PropertyControlConstants"; import type { AnvilConfig, AutocompletionDefinitions, @@ -32,6 +35,7 @@ import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; import { Colors } from "constants/Colors"; import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants"; import { + DEFAULT_CONTENT_PADDING, GridDefaults, WIDGET_TAGS, WidgetHeightLimits, @@ -102,6 +106,7 @@ class TabsWidget extends BaseWidget< borderWidth: 1, borderColor: Colors.GREY_5, backgroundColor: Colors.WHITE, + contentPadding: DEFAULT_CONTENT_PADDING, minDynamicHeight: WidgetHeightLimits.MIN_CANVAS_HEIGHT_IN_ROWS + 5, tabsObj: { tab1: { @@ -464,6 +469,27 @@ class TabsWidget extends BaseWidget< validation: { type: ValidationTypes.NUMBER }, postUpdateAction: ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT, }, + { + helpText: + "Inner padding (px). Use one value for all sides or 2–4 values (e.g. 10 20 10 20 for top right bottom left)", + propertyName: "contentPadding", + label: "Padding (px)", + placeholderText: "e.g. 10 or 10 20 10 20", + controlType: "INPUT_TEXT", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: contentPaddingValidation, + default: DEFAULT_CONTENT_PADDING, + expected: { + type: "1–4 space-separated numbers (px)", + example: "10 or 10 20 10 20", + }, + }, + } as ValidationConfig, + }, { propertyName: "borderRadius", label: "Border radius", @@ -572,6 +598,10 @@ class TabsWidget extends BaseWidget< isAutoHeightEnabledForWidget(this.props) && !isAutoHeightEnabledForWidgetWithLimits(this.props); + const contentPaddingPx = parseContentPadding( + this.props.contentPadding ?? DEFAULT_CONTENT_PADDING, + ); + return ( Date: Mon, 16 Mar 2026 21:29:26 -0500 Subject: [PATCH 4/5] refactor(widget): update section names and help texts for clarity - Changed section name from "Border and shadow" to "Border, shadow & padding" across multiple widgets for consistency. - Enhanced help texts for `contentPadding` in `ContainerWidget`, `JSONFormWidget`, and `ListWidgetV2` to provide clearer instructions on padding usage. These updates improve the user experience by ensuring better understanding and consistency in widget configurations. --- app/client/src/widgets/ContainerWidget/widget/index.tsx | 4 ++-- .../src/widgets/JSONFormWidget/widget/propertyConfig.ts | 4 ++-- app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts | 4 ++-- app/client/src/widgets/TabsWidget/widget/index.tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/client/src/widgets/ContainerWidget/widget/index.tsx b/app/client/src/widgets/ContainerWidget/widget/index.tsx index b510f6d4e94c..7300dfd2fe0c 100644 --- a/app/client/src/widgets/ContainerWidget/widget/index.tsx +++ b/app/client/src/widgets/ContainerWidget/widget/index.tsx @@ -284,7 +284,7 @@ export class ContainerWidget extends BaseWidget< ], }, { - sectionName: "Border and shadow", + sectionName: "Border, shadow & padding", children: [ { helpText: "Enter value for border width", @@ -299,7 +299,7 @@ export class ContainerWidget extends BaseWidget< }, { helpText: - "Inner padding (px). Use one value for all sides or 2–4 values (e.g. 10 20 10 20 for top right bottom left)", + "Space between the border and the content, in pixels. Use one value for all sides, or 2–4 values for top, right, bottom, left (e.g. 10 20 10 20).", propertyName: "contentPadding", label: "Padding (px)", placeholderText: "e.g. 10 or 10 20 10 20", diff --git a/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts b/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts index ad2711277b50..832f62460998 100644 --- a/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts +++ b/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts @@ -661,7 +661,7 @@ export const styleConfig = [ ], }, { - sectionName: "Border and shadow", + sectionName: "Border, shadow & padding", children: [ { propertyName: "borderWidth", @@ -675,7 +675,7 @@ export const styleConfig = [ }, { helpText: - "Inner padding (px). Use one value for all sides or 2–4 values (e.g. 10 20 10 20 for top right bottom left)", + "Space between the border and the form body (where fields render), in pixels. Use one value for all sides, or 2–4 values for top, right, bottom, left (e.g. 10 20 10 20).", propertyName: "contentPadding", label: "Padding (px)", placeholderText: "e.g. 10 or 10 20 10 20", diff --git a/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts b/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts index cca36110f8c4..a28866a2592f 100644 --- a/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts +++ b/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts @@ -475,11 +475,11 @@ export const PropertyPaneStyleConfig = [ ], }, { - sectionName: "Border and shadow", + sectionName: "Border, shadow & padding", children: [ { helpText: - "Inner padding (px). Use one value for all sides or 2–4 values (e.g. 10 20 10 20 for top right bottom left). Applies to the list container only.", + "Space between the list border and the scrollable content, in pixels. Use one value for all sides, or 2–4 values for top, right, bottom, left (e.g. 10 20 10 20). Does not affect spacing between list items.", propertyName: "contentPadding", label: "Padding (px)", placeholderText: "e.g. 10 or 10 20 10 20", diff --git a/app/client/src/widgets/TabsWidget/widget/index.tsx b/app/client/src/widgets/TabsWidget/widget/index.tsx index 1eb83c4908b1..8eca05158e98 100644 --- a/app/client/src/widgets/TabsWidget/widget/index.tsx +++ b/app/client/src/widgets/TabsWidget/widget/index.tsx @@ -425,7 +425,7 @@ class TabsWidget extends BaseWidget< static getPropertyPaneStyleConfig() { return [ { - sectionName: "Colors, Borders and Shadows", + sectionName: "Colors, border, shadow & padding", children: [ { propertyName: "accentColor", @@ -471,7 +471,7 @@ class TabsWidget extends BaseWidget< }, { helpText: - "Inner padding (px). Use one value for all sides or 2–4 values (e.g. 10 20 10 20 for top right bottom left)", + "Space between the border and the tab content, in pixels. Use one value for all sides, or 2–4 values for top, right, bottom, left (e.g. 10 20 10 20).", propertyName: "contentPadding", label: "Padding (px)", placeholderText: "e.g. 10 or 10 20 10 20", From 6c9b35482a8c38d250f3d66ab3b3751da0c367a7 Mon Sep 17 00:00:00 2001 From: Luis Ibarra Date: Mon, 16 Mar 2026 21:56:55 -0500 Subject: [PATCH 5/5] feat(validation): implement shared padding validation function - Added `validatePaddingString` function to validate and parse content padding inputs, ensuring they consist of 1 to 4 non-negative space-separated numbers. - Removed the deprecated `contentPaddingValidation` function from `WidgetUtils` and updated relevant widgets (`ContainerWidget`, `JSONFormWidget`, `ListWidgetV2`, `TabsWidget`) to utilize the new validation function. - Enhanced property configurations to reflect the new validation logic, improving consistency and usability across widgets. This change streamlines padding validation and enhances the user experience by providing a unified approach to padding input validation. --- app/client/src/utils/paddingValidation.ts | 59 +++++++++ .../widgets/ContainerWidget/widget/index.tsx | 12 +- .../JSONFormWidget/widget/propertyConfig.ts | 10 +- .../ListWidgetV2/widget/propertyConfig.ts | 10 +- .../src/widgets/TabsWidget/widget/index.tsx | 10 +- app/client/src/widgets/WidgetUtils.test.ts | 121 ------------------ app/client/src/widgets/WidgetUtils.ts | 62 --------- 7 files changed, 81 insertions(+), 203 deletions(-) create mode 100644 app/client/src/utils/paddingValidation.ts diff --git a/app/client/src/utils/paddingValidation.ts b/app/client/src/utils/paddingValidation.ts new file mode 100644 index 000000000000..df5174fa0b2f --- /dev/null +++ b/app/client/src/utils/paddingValidation.ts @@ -0,0 +1,59 @@ +import type { ValidationResponse } from "constants/WidgetValidation"; + +/** + * Shared validation for contentPadding-style properties. + * + * - Accepts 1–4 space-separated numbers (px), each >= 0 + * - Returns { isValid, parsed, messages? } in the shape expected by + * FUNCTION-type widget property validations. + * + * The default value is provided by the caller so different widgets + * (e.g. JSON Form vs Container) can use different defaults. + */ +export function validatePaddingString( + value: unknown, + defaultValue: string, +): ValidationResponse { + const raw = + value == null ? "" : typeof value === "string" ? value : String(value); + const trimmed = raw.trim(); + + if (!trimmed) { + return { isValid: true, parsed: defaultValue }; + } + + const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0); + + if (tokens.length < 1 || tokens.length > 4) { + return { + isValid: false, + parsed: defaultValue, + messages: [ + { + name: "ValidationError", + message: + "Enter 1 to 4 space-separated numbers (e.g. 10 or 10 20 10 20).", + }, + ], + }; + } + + for (const token of tokens) { + const n = parseFloat(token); + + if (Number.isNaN(n) || n < 0) { + return { + isValid: false, + parsed: defaultValue, + messages: [ + { + name: "ValidationError", + message: "Each value must be a non-negative number.", + }, + ], + }; + } + } + + return { isValid: true, parsed: trimmed }; +} diff --git a/app/client/src/widgets/ContainerWidget/widget/index.tsx b/app/client/src/widgets/ContainerWidget/widget/index.tsx index 7300dfd2fe0c..0766dc28c0fc 100644 --- a/app/client/src/widgets/ContainerWidget/widget/index.tsx +++ b/app/client/src/widgets/ContainerWidget/widget/index.tsx @@ -5,7 +5,6 @@ import type { ContainerStyle } from "../component"; import ContainerComponent from "../component"; import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; -import type { ValidationConfig } from "constants/PropertyControlConstants"; import { ValidationTypes } from "constants/WidgetValidation"; import { compact, get, map, sortBy } from "lodash"; import WidgetsMultiSelectBox from "layoutSystems/fixedlayout/common/widgetGrouping/WidgetsMultiSelectBox"; @@ -13,11 +12,11 @@ import type { SetterConfig, Stylesheet } from "entities/AppTheming"; import { getSnappedGrid } from "sagas/WidgetOperationUtils"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { - contentPaddingValidation, isAutoHeightEnabledForWidget, DefaultAutocompleteDefinitions, isAutoHeightEnabledForWidgetWithLimits, } from "widgets/WidgetUtils"; +import { validatePaddingString } from "utils/paddingValidation"; import { BlueprintOperationTypes, type AnvilConfig, @@ -38,6 +37,7 @@ import { GridDefaults, WidgetHeightLimits, } from "constants/WidgetConstants"; +import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import { FlexVerticalAlignment, Positioning, @@ -309,14 +309,16 @@ export class ContainerWidget extends BaseWidget< validation: { type: ValidationTypes.FUNCTION, params: { - fn: contentPaddingValidation, - default: DEFAULT_CONTENT_PADDING, + fn: (value: unknown) => + validatePaddingString(value, DEFAULT_CONTENT_PADDING), expected: { type: "1–4 space-separated numbers (px)", example: "10 or 10 20 10 20", + // Keep default autocomplete type for consistency + autocompleteDataType: AutocompleteDataType.STRING, }, }, - } as ValidationConfig, + }, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts b/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts index 832f62460998..1ab1b6c5d818 100644 --- a/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts +++ b/app/client/src/widgets/JSONFormWidget/widget/propertyConfig.ts @@ -16,8 +16,8 @@ import { SUCCESSFULL_BINDING_MESSAGE, } from "../constants/messages"; import { createMessage } from "ee/constants/messages"; -import type { ValidationConfig } from "constants/PropertyControlConstants"; -import { contentPaddingValidation } from "widgets/WidgetUtils"; +import { validatePaddingString } from "utils/paddingValidation"; +import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import { FieldOptionsType } from "components/editorComponents/WidgetQueryGeneratorForm/WidgetSpecificControls/OtherFields/Field/Dropdown/types"; import { DROPDOWN_VARIANT } from "components/editorComponents/WidgetQueryGeneratorForm/CommonControls/DatasourceDropdown/types"; @@ -685,14 +685,14 @@ export const styleConfig = [ validation: { type: ValidationTypes.FUNCTION, params: { - fn: contentPaddingValidation, - default: "25", + fn: (value: unknown) => validatePaddingString(value, "25"), expected: { type: "1–4 space-separated numbers (px)", example: "10 or 10 20 10 20", + autocompleteDataType: AutocompleteDataType.STRING, }, }, - } as ValidationConfig, + }, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts b/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts index a28866a2592f..dd6e32c994f0 100644 --- a/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts +++ b/app/client/src/widgets/ListWidgetV2/widget/propertyConfig.ts @@ -13,9 +13,8 @@ import { LIST_WIDGET_V2_TOTAL_RECORD_TOOLTIP, createMessage, } from "ee/constants/messages"; -import type { ValidationConfig } from "constants/PropertyControlConstants"; import { DEFAULT_CONTENT_PADDING } from "constants/WidgetConstants"; -import { contentPaddingValidation } from "widgets/WidgetUtils"; +import { validatePaddingString } from "utils/paddingValidation"; const MIN_ITEM_SPACING = 0; const MAX_ITEM_SPACING = 16; @@ -489,14 +488,15 @@ export const PropertyPaneStyleConfig = [ validation: { type: ValidationTypes.FUNCTION, params: { - fn: contentPaddingValidation, - default: DEFAULT_CONTENT_PADDING, + fn: (value: unknown) => + validatePaddingString(value, DEFAULT_CONTENT_PADDING), expected: { type: "1–4 space-separated numbers (px)", example: "10 or 10 20 10 20", + autocompleteDataType: AutocompleteDataType.STRING, }, }, - } as ValidationConfig, + }, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/TabsWidget/widget/index.tsx b/app/client/src/widgets/TabsWidget/widget/index.tsx index 8eca05158e98..f6901d73b619 100644 --- a/app/client/src/widgets/TabsWidget/widget/index.tsx +++ b/app/client/src/widgets/TabsWidget/widget/index.tsx @@ -20,13 +20,12 @@ import type { TabContainerWidgetProps, TabsWidgetProps } from "../constants"; import derivedProperties from "./parseDerivedProperties"; import type { SetterConfig, Stylesheet } from "entities/AppTheming"; import { - contentPaddingValidation, isAutoHeightEnabledForWidget, isAutoHeightEnabledForWidgetWithLimits, DefaultAutocompleteDefinitions, parseContentPadding, } from "widgets/WidgetUtils"; -import type { ValidationConfig } from "constants/PropertyControlConstants"; +import { validatePaddingString } from "utils/paddingValidation"; import type { AnvilConfig, AutocompletionDefinitions, @@ -481,14 +480,15 @@ class TabsWidget extends BaseWidget< validation: { type: ValidationTypes.FUNCTION, params: { - fn: contentPaddingValidation, - default: DEFAULT_CONTENT_PADDING, + fn: (value: unknown) => + validatePaddingString(value, DEFAULT_CONTENT_PADDING), expected: { type: "1–4 space-separated numbers (px)", example: "10 or 10 20 10 20", + autocompleteDataType: AutocompleteDataType.STRING, }, }, - } as ValidationConfig, + }, }, { propertyName: "borderRadius", diff --git a/app/client/src/widgets/WidgetUtils.test.ts b/app/client/src/widgets/WidgetUtils.test.ts index 43e1eabcac15..ce0a1310a356 100644 --- a/app/client/src/widgets/WidgetUtils.test.ts +++ b/app/client/src/widgets/WidgetUtils.test.ts @@ -28,7 +28,6 @@ import { getWidgetMinAutoHeight, isCompactMode, parseContentPadding, - contentPaddingValidation, } from "./WidgetUtils"; import { getCustomTextColor, @@ -792,123 +791,3 @@ describe("parseContentPadding", () => { expect(parseContentPadding("invalid", fb)).toEqual([0, 0, 0, 0]); }); }); - -describe("contentPaddingValidation", () => { - const noop = {} as Record; - const defaultConfig = { params: { default: "4" } }; - - it("accepts 1 value", () => { - const r = contentPaddingValidation( - "10", - noop, - null, - null, - "", - defaultConfig, - ); - - expect(r.isValid).toBe(true); - expect(r.parsed).toBe("10"); - }); - - it("accepts 2 values", () => { - const r = contentPaddingValidation( - "10 20", - noop, - null, - null, - "", - defaultConfig, - ); - - expect(r.isValid).toBe(true); - expect(r.parsed).toBe("10 20"); - }); - - it("accepts 3 values", () => { - const r = contentPaddingValidation( - "10 20 30", - noop, - null, - null, - "", - defaultConfig, - ); - - expect(r.isValid).toBe(true); - expect(r.parsed).toBe("10 20 30"); - }); - - it("accepts 4 values", () => { - const r = contentPaddingValidation( - "10 20 10 20", - noop, - null, - null, - "", - defaultConfig, - ); - - expect(r.isValid).toBe(true); - expect(r.parsed).toBe("10 20 10 20"); - }); - - it("returns valid with default for empty string", () => { - const r = contentPaddingValidation("", noop, null, null, "", defaultConfig); - - expect(r.isValid).toBe(true); - expect(r.parsed).toBe("4"); - }); - - it("rejects more than 4 tokens", () => { - const r = contentPaddingValidation( - "10 20 30 40 50", - noop, - null, - null, - "", - defaultConfig, - ); - - expect(r.isValid).toBe(false); - expect(r.parsed).toBe("4"); - expect(r.messages?.length).toBeGreaterThan(0); - }); - - it("rejects non-numeric token", () => { - const r = contentPaddingValidation( - "10 abc 20", - noop, - null, - null, - "", - defaultConfig, - ); - - expect(r.isValid).toBe(false); - expect(r.parsed).toBe("4"); - }); - - it("rejects negative value", () => { - const r = contentPaddingValidation( - "10 -5 20", - noop, - null, - null, - "", - defaultConfig, - ); - - expect(r.isValid).toBe(false); - expect(r.parsed).toBe("4"); - }); - - it("uses config default when empty", () => { - const r = contentPaddingValidation("", noop, null, null, "", { - params: { default: "25" }, - }); - - expect(r.isValid).toBe(true); - expect(r.parsed).toBe("25"); - }); -}); diff --git a/app/client/src/widgets/WidgetUtils.ts b/app/client/src/widgets/WidgetUtils.ts index 2b80cab59b02..b8a43681c5cf 100644 --- a/app/client/src/widgets/WidgetUtils.ts +++ b/app/client/src/widgets/WidgetUtils.ts @@ -125,68 +125,6 @@ export function parseContentPadding( } } -/** - * Validation for contentPadding property: 1–4 space-separated numbers (px), each >= 0. - * Rejects empty tokens, non-numeric, negative, or wrong token count. - * Used by Container and Form (and other container-like widgets) in property pane. - */ -export function contentPaddingValidation( - value: unknown, - _props?: Record, - _lodash?: unknown, - _moment?: unknown, - _propertyPath?: string, - config?: { params?: { default?: string } }, -): { - isValid: boolean; - parsed: string; - messages?: Array<{ name: string; message: string }>; -} { - const defaultPadding = config?.params?.default ?? "4"; - const str = - value == null ? "" : typeof value === "string" ? value : String(value); - const trimmed = str.trim(); - - if (trimmed === "") { - return { isValid: true, parsed: defaultPadding }; - } - - const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0); - - if (tokens.length < 1 || tokens.length > 4) { - return { - isValid: false, - parsed: defaultPadding, - messages: [ - { - name: "ValidationError", - message: - "Enter 1 to 4 space-separated numbers (e.g. 10 or 10 20 10 20)", - }, - ], - }; - } - - for (let i = 0; i < tokens.length; i++) { - const n = parseFloat(tokens[i]); - - if (Number.isNaN(n) || n < 0) { - return { - isValid: false, - parsed: defaultPadding, - messages: [ - { - name: "ValidationError", - message: "Each value must be a non-negative number", - }, - ], - }; - } - } - - return { isValid: true, parsed: trimmed }; -} - export const DefaultAutocompleteDefinitions = { isVisible: { "!type": "bool",