Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/client/src/constants/WidgetConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
59 changes: 59 additions & 0 deletions app/client/src/utils/paddingValidation.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
16 changes: 15 additions & 1 deletion app/client/src/widgets/ContainerWidget/component/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) => {
Expand All @@ -55,6 +62,7 @@ interface ContainerWrapperProps {
dropDisabled?: boolean;
$noScroll: boolean;
forceFullOpacity?: boolean;
contentPaddingPx: [number, number, number, number];
}

function ContainerComponentWrapper(
Expand Down Expand Up @@ -134,6 +142,7 @@ function ContainerComponentWrapper(
? "auto-layout"
: ""
}`}
contentPaddingPx={props.contentPaddingPx}
dropDisabled={props.dropDisabled}
forceFullOpacity={props.forceFullOpacity}
onClick={props.onClick}
Expand All @@ -151,10 +160,13 @@ function ContainerComponentWrapper(
}

function ContainerComponent(props: ContainerComponentProps) {
const contentPaddingPx = parseContentPadding(props.contentPadding ?? "");

if (props.detachFromLayout) {
return (
<ContainerComponentWrapper
$noScroll={!!props.noScroll}
contentPaddingPx={contentPaddingPx}
dropDisabled={props.dropDisabled}
forceFullOpacity={props.forceFullOpacity}
onClick={props.onClick}
Expand Down Expand Up @@ -187,6 +199,7 @@ function ContainerComponent(props: ContainerComponentProps) {
<ContainerComponentWrapper
$noScroll={!!props.noScroll}
backgroundColor={props.backgroundColor}
contentPaddingPx={contentPaddingPx}
dropDisabled={props.dropDisabled}
forceFullOpacity={props.forceFullOpacity}
onClick={props.onClick}
Expand All @@ -212,6 +225,7 @@ export type ContainerStyle = "border" | "card" | "rounded-border" | "none";

export interface ContainerComponentProps extends WidgetStyleContainerProps {
children?: ReactNode;
contentPadding?: string;
shouldScrollContents?: boolean;
resizeDisabled?: boolean;
selected?: boolean;
Expand Down
50 changes: 41 additions & 9 deletions app/client/src/widgets/ContainerWidget/widget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DefaultAutocompleteDefinitions,
isAutoHeightEnabledForWidgetWithLimits,
} from "widgets/WidgetUtils";
import { validatePaddingString } from "utils/paddingValidation";
import {
BlueprintOperationTypes,
type AnvilConfig,
Expand All @@ -31,7 +32,12 @@ import ThumbnailSVG from "../thumbnail.svg";
import { ButtonBoxShadowTypes } from "components/constants";
import { Colors } from "constants/Colors";
import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants";
import { GridDefaults, WidgetHeightLimits } from "constants/WidgetConstants";
import {
DEFAULT_CONTENT_PADDING,
GridDefaults,
WidgetHeightLimits,
} from "constants/WidgetConstants";
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
import {
FlexVerticalAlignment,
Positioning,
Expand Down Expand Up @@ -100,6 +106,7 @@ export class ContainerWidget extends BaseWidget<
containerStyle: "card",
borderColor: Colors.GREY_5,
borderWidth: "1",
contentPadding: DEFAULT_CONTENT_PADDING,
boxShadow: ButtonBoxShadowTypes.NONE,
animateLoading: true,
children: [],
Expand Down Expand Up @@ -277,7 +284,7 @@ export class ContainerWidget extends BaseWidget<
],
},
{
sectionName: "Border and shadow",
sectionName: "Border, shadow & padding",
children: [
{
helpText: "Enter value for border width",
Expand All @@ -290,6 +297,29 @@ export class ContainerWidget extends BaseWidget<
validation: { type: ValidationTypes.NUMBER },
postUpdateAction: ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT,
},
{
helpText:
"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",
controlType: "INPUT_TEXT",
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.FUNCTION,
params: {
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,
},
},
},
},
{
propertyName: "borderRadius",
label: "Border radius",
Expand Down Expand Up @@ -367,15 +397,16 @@ export class ContainerWidget extends BaseWidget<
}

renderChildren = () => {
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) => (
<React.Fragment key={child.widgetId}>
{this.renderChildWidget(child)}
</React.Fragment>
));
};

renderAsContainerComponent(props: ContainerWidgetProps<WidgetProps>) {
Expand Down Expand Up @@ -411,6 +442,7 @@ export interface ContainerWidgetProps<T extends WidgetProps>
extends WidgetProps {
children?: T[];
containerStyle?: ContainerStyle;
contentPadding?: string;
onClick?: MouseEventHandler<HTMLDivElement>;
onClickCapture?: MouseEventHandler<HTMLDivElement>;
shouldScrollContents?: boolean;
Expand Down
7 changes: 6 additions & 1 deletion app/client/src/widgets/FormWidget/widget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 19 additions & 10 deletions app/client/src/widgets/JSONFormWidget/component/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValues = any> = PropsWithChildren<{
backgroundColor?: string;
contentPaddingPx?: [number, number, number, number];
disabledWhenInvalid?: boolean;
fixedFooter: boolean;
getFormData: () => TValues;
Expand Down Expand Up @@ -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";

Expand All @@ -71,8 +76,8 @@ const StyledFormFooter = styled.div<StyledFooterProps>`
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%;

Expand Down Expand Up @@ -104,10 +109,11 @@ const StyledTitle = styled(Text)<{
margin-bottom: ${TITLE_MARGIN_BOTTOM}px;
`;

const StyledFormBody = styled.div<StyledFormBodyProps>`
const StyledFormBody = styled.div<StyledFormBodyPropsWithPadding>`
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``;
Expand All @@ -124,6 +130,7 @@ function Form<TValues = any>(
{
backgroundColor,
children,
contentPaddingPx,
disabledWhenInvalid,
fixedFooter,
getFormData,
Expand Down Expand Up @@ -281,6 +288,7 @@ function Form<TValues = any>(
scrollContents={scrollContents}
>
<StyledFormBody
$contentPaddingPx={contentPaddingPx ?? DEFAULT_FORM_PADDING_PX}
className="t--jsonform-body"
stretchBodyVertically={stretchBodyVertically}
>
Expand All @@ -289,6 +297,7 @@ function Form<TValues = any>(
</StyledFormBody>
{!hideFooter && (
<StyledFormFooter
$contentPaddingPx={contentPaddingPx ?? DEFAULT_FORM_PADDING_PX}
backgroundColor={backgroundColor}
className="t--jsonform-footer"
fixedFooter={fixedFooter}
Expand Down
2 changes: 2 additions & 0 deletions app/client/src/widgets/JSONFormWidget/component/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface JSONFormComponentProps<TValues = any> {
borderWidth?: number;
boxShadow?: BoxShadow;
boxShadowColor?: string;
contentPaddingPx?: [number, number, number, number];
disabledWhenInvalid?: boolean;
executeAction: (action: Action) => void;
fieldLimitExceeded: boolean;
Expand Down Expand Up @@ -213,6 +214,7 @@ function JSONFormComponent<TValues>(
<StyledContainer backgroundColor={backgroundColor} {...styleProps}>
<Form
backgroundColor={backgroundColor}
contentPaddingPx={rest.contentPaddingPx}
disabledWhenInvalid={rest.disabledWhenInvalid}
fixedFooter={rest.fixedFooter}
getFormData={getFormData}
Expand Down
Loading
Loading