feat: Anvil themeing and Anvil vertical alignment (#29907)

## Description
This PR adds the features of proper vertical alignment and themeing to
Anvil.
- A separate `Container` component is created for Anvil, that is used as
the layer on top of which the themeing tokens are applied.
- A default `min-height` is set using tokens for all widgets in Anvil. 
- Anvil now stops considering any `min-height` configurations provided
by the widgets. It is the widgets responsibility to take care of their
own heights, and Anvil will accommodate them -- no matter the height.
- Table widget's default height is now set to the min height that was
configured for it earlier.
- `AnvilFlexComponent` now has `overflow:visible` to allow the shadows
for zones and sections to not be cut-off.
- All widgets are aligned center vertically by default. This will apply
if they're smaller than the set `min-height`
- Zones and Sections have elevation styles applied suing the `Container`
component mentioned above.
- Zones and Sections don't have any styling property other than
`Background`, we'll add more as we understand more about the product.
   

> Conditional vertical margin applied to widgets.
> If in a row of widgets (.aligned-widget-row), one of the widgets has a
label ([data-field-label-wrapper]), then
> all widgets (.anvil-widget-wrapper) in the row other than the widget
with the label, will shift down using the
> margin-block-start property. This is to ensure that the widgets are
aligned vertically.
> The value of the margin-block-start property is calculated based on
the spacing tokens used by the labels in input
>     like components
> 

#### PR fixes following issue(s)
Fixes #29073 
Fixes #28591
Fixes #28592 
Fixes #28593


#### Media
![Screenshot 2023-12-28 at 3 55
05 AM](https://github.com/appsmithorg/appsmith/assets/103687/30b04bc7-62af-40af-9f4c-50774bec3fdf)


#### Type of change
- New feature (non-breaking change which adds functionality)
## Testing
#### How Has This Been Tested?
- [x] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress

#### Test Plan

#### Issues raised during DP testing
## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced conditional vertical margins for widgets to ensure
alignment within rows.
- Added a new `Container` component for thematic elevation styles in
Anvil widgets.
- Implemented elevation style options and semantic background settings
for Section and Zone widgets.

- **Enhancements**
- Improved visual layout and alignment of AnvilFlexComponent with
updated styling properties.
- Added `className` properties to various layout components for enhanced
CSS targeting.

- **Style**
- Updated widget styles to accommodate new background and elevation
features.

- **Refactor**
	- Simplified padding logic in WDSParagraphWidget.
	- Streamlined dimensions calculation in WDSTableWidget.

- **Documentation**
- Renamed sections in property panes to better reflect background
styling options.

- **Chores**
- Added `Elevations` enum to manage elevation values consistently across
components.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Abhinav Jha 2023-12-29 08:41:05 +05:30 committed by GitHub
parent dbda916f09
commit e05313c943
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 125 additions and 135 deletions

View File

@ -248,3 +248,15 @@ div.bp3-popover-arrow {
.reconnect-datasource-modal {
z-index: 9 !important;
}
/** Conditional vertical margin applied to widgets.
If in a row of widgets (.aligned-widget-row), one of the widgets has a label ([data-field-label-wrapper]), then
all widgets (.anvil-widget-wrapper) in the row other than the widget with the label, will shift down using the
margin-block-start property. This is to ensure that the widgets are aligned vertically.
The value of the margin-block-start property is calculated based on the spacing tokens used by the labels in input like components
*/
.aligned-widget-row:has(.anvil-widget-wrapper [data-field-label-wrapper])
.anvil-widget-wrapper:not(:has([data-field-label-wrapper])) {
margin-block-start: calc(var(--inner-spacing-2) + var(--sizing-3));
}

View File

@ -107,7 +107,7 @@ export function AnvilFlexComponent(props: AnvilFlexComponentProps) {
props.widgetType,
)} t--widget-${props.widgetName.toLowerCase()} drop-target-${
props.layoutId
} row-index-${props.rowIndex}`,
} row-index-${props.rowIndex} anvil-widget-wrapper`,
[
props.parentId,
props.widgetId,
@ -129,6 +129,8 @@ export function AnvilFlexComponent(props: AnvilFlexComponentProps) {
height: "auto",
padding: "spacing-1",
width: "auto",
minHeight: { base: "var(--sizing-12)" },
alignItems: "center",
};
if (props?.widgetSize) {
// adding min max limits only if they are available, as WDS Flex doesn't handle undefined values.
@ -138,9 +140,7 @@ export function AnvilFlexComponent(props: AnvilFlexComponentProps) {
if (validateResponsiveProp(props.widgetSize?.maxWidth)) {
data.maxWidth = props.widgetSize.maxWidth;
}
if (validateResponsiveProp(props.widgetSize?.minHeight)) {
data.minHeight = props.widgetSize.minHeight;
}
if (validateResponsiveProp(props.widgetSize?.minWidth)) {
// Setting a base of 100% for Fill widgets to ensure that they expand on smaller sizes.
data.minWidth = getResponsiveMinWidth(
@ -158,7 +158,7 @@ export function AnvilFlexComponent(props: AnvilFlexComponentProps) {
return {
position: "relative",
// overflow is set to make sure widgets internal components/divs don't overflow this boundary causing scrolls
overflow: "hidden",
overflow: "visible",
opacity: (isDragging && isSelected) || !props.isVisible ? 0.5 : 1,
"&:hover": {
zIndex: onHoverZIndex,

View File

@ -24,6 +24,7 @@ class AlignedWidgetRow extends BaseLayoutComponent {
alignSelf: "stretch",
direction: "row",
wrap: "wrap",
className: "aligned-widget-row",
};
}
}

View File

@ -18,7 +18,7 @@ import type {
} from "layoutSystems/anvil/utils/types";
import { usePositionObserver } from "layoutSystems/common/utils/LayoutElementPositionsObserver/usePositionObserver";
import { getAnvilLayoutDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils";
import { type RenderMode, RenderModes } from "constants/WidgetConstants";
import type { RenderMode } from "constants/WidgetConstants";
import type { LayoutComponentTypes } from "layoutSystems/anvil/utils/anvilTypes";
export const FLEX_LAYOUT_PADDING = 4;
@ -54,6 +54,7 @@ export interface FlexLayoutProps
rowGap?: Responsive<SpacingDimension>;
padding?: Responsive<SpacingDimension>;
width?: Responsive<SizingDimension>;
className?: string;
}
export const FlexLayout = React.memo((props: FlexLayoutProps) => {
@ -62,6 +63,7 @@ export const FlexLayout = React.memo((props: FlexLayoutProps) => {
border,
canvasId,
children,
className,
columnGap,
direction,
flexBasis,
@ -118,10 +120,11 @@ export const FlexLayout = React.memo((props: FlexLayoutProps) => {
maxWidth: maxWidth || "none",
minHeight: minHeight || "unset",
minWidth: minWidth || "unset",
padding: padding || (isDropTarget ? `${FLEX_LAYOUT_PADDING}px` : "0px"),
padding: padding || (isDropTarget ? `spacing-0` : "0px"),
rowGap: rowGap || "0px",
width: width || "auto",
wrap: wrap || "nowrap",
className: className || "",
};
}, [
alignSelf,
@ -146,17 +149,12 @@ export const FlexLayout = React.memo((props: FlexLayoutProps) => {
// The following properties aren't included in type FlexProps but can be passed as style.
const styleProps: CSSProperties = useMemo(() => {
return {
border:
border ||
(isDropTarget && renderMode === RenderModes.CANVAS
? "1px dashed #979797"
: "none"),
position: position || "relative",
};
}, [border, isDropTarget, position, renderMode]);
const className = useMemo(() => {
return `layout-${layoutId} layout-index-${layoutIndex} ${
const _className = useMemo(() => {
return `${className} layout-${layoutId} layout-index-${layoutIndex} ${
isContainer ? "make-container" : ""
}`;
}, [isContainer, layoutId, layoutIndex]);
@ -164,7 +162,7 @@ export const FlexLayout = React.memo((props: FlexLayoutProps) => {
return (
<Flex
{...flexProps}
className={className}
className={_className}
id={getAnvilLayoutDOMId(canvasId, layoutId)}
ref={ref}
style={styleProps}

View File

@ -60,6 +60,7 @@ export interface LayoutComponentProps extends LayoutProps {
layoutIndex: number; // Index of the layout component in the parent layout.
layoutOrder: string[]; // Top - down hierarchy of layoutIds.
parentDropTarget: string; // layoutId of the immediate drop target parent. Could be self as well.
className?: string;
renderMode: RenderMode;
}

View File

@ -134,6 +134,7 @@ export function renderWidgetsInAlignedRow(
parentDropTarget: props.parentDropTarget,
renderMode: props.renderMode,
wrap: { base: "wrap", [`${MOBILE_BREAKPOINT}px`]: "nowrap" },
className: props.className,
};
const startChildren: WidgetLayoutProps[] = (

View File

@ -0,0 +1,57 @@
import type { ReactNode } from "react";
import React from "react";
import styled from "styled-components";
import { generateClassName } from "utils/generators";
import { Elevations } from "./constants";
/**
* This container component wraps the Zone and Section widgets and allows Anvil to utilise tokens from the themes
* `elevatedBackground` is a boolean value that describes whether the elevation styles should be applied
* `elevation` is the value of the elevation. Zones have a higher elevation than Sections.
*
* Also, in the case of zones, we're removing all padding.
*/
const StyledContainerComponent = styled.div<
Omit<ContainerComponentProps, "widgetId">
>`
height: 100%;
width: 100%;
overflow: hidden;
outline: none;
border: none;
position: relative;
${(props) =>
props.elevatedBackground
? `background: var(--color-bg-elevation-${props.elevation}); box-shadow: var(--box-shadow-${props.elevation});`
: ""}
border-radius: var(--border-radius-1);
padding-block: var(--outer-spacing-1);
padding-inline: var(--outer-spacing-1);
${(props) =>
props.elevation === Elevations.SECTION_ELEVATION
? `padding-block: var(--outer-spacing-0); padding-inline: var(--outer-spacing-0);`
: ""}
border-width: var(--border-width-1);
`;
export function ContainerComponent(props: ContainerComponentProps) {
return (
<StyledContainerComponent
className={`${generateClassName(props.widgetId)}`}
elevatedBackground={props.elevatedBackground}
elevation={props.elevation}
>
{props.children}
</StyledContainerComponent>
);
}
export interface ContainerComponentProps {
widgetId: string;
children?: ReactNode;
elevation: Elevations;
elevatedBackground: boolean;
}

View File

@ -12,7 +12,7 @@ import { sectionPreset } from "layoutSystems/anvil/layoutComponents/presets/sect
import { ButtonBoxShadowTypes } from "components/constants";
export const defaultConfig: WidgetDefaultProps = {
backgroundColor: "lightslategrey",
elevatedBackground: false,
borderRadius: "0.375rem",
boxShadow: ButtonBoxShadowTypes.NONE,
children: [],

View File

@ -2,64 +2,20 @@ import { ValidationTypes } from "constants/WidgetValidation";
export const propertyPaneStyle = [
{
sectionName: "Color",
sectionName: "Background",
children: [
{
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
propertyName: "backgroundColor",
label: "Background color",
controlType: "COLOR_PICKER",
propertyName: "elevatedBackground",
label: "Background",
controlType: "SWITCH",
fullWidth: true,
helpText: "Sets the semantic elevated background of the section",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
propertyName: "borderColor",
label: "Border color",
controlType: "COLOR_PICKER",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
{
sectionName: "Border and shadow",
children: [
{
helpText: "Enter value for border width",
propertyName: "borderWidth",
label: "Border width",
placeholderText: "Enter value in px",
controlType: "INPUT_TEXT",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.NUMBER },
},
{
propertyName: "borderRadius",
label: "Border radius",
helpText: "Rounds the corners of the icon button's outer border edge",
controlType: "BORDER_RADIUS_OPTIONS",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "boxShadow",
label: "Box shadow",
helpText:
"Enables you to cast a drop shadow from the frame of the widget",
controlType: "BOX_SHADOW_OPTIONS",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
validation: {
type: ValidationTypes.BOOLEAN,
},
},
],
},

View File

@ -20,9 +20,9 @@ import type { LayoutProps } from "layoutSystems/anvil/utils/anvilTypes";
import BaseWidget from "widgets/BaseWidget";
import type { ReactNode } from "react";
import React from "react";
import ContainerComponent from "widgets/ContainerWidget/component";
import { ContainerComponent } from "widgets/anvil/Container";
import { LayoutProvider } from "layoutSystems/anvil/layoutComponents/LayoutProvider";
import { anvilWidgets } from "widgets/anvil/constants";
import { Elevations, anvilWidgets } from "widgets/anvil/constants";
class SectionWidget extends BaseWidget<SectionWidgetProps, WidgetState> {
static type = anvilWidgets.SECTION_WIDGET;
@ -83,7 +83,11 @@ class SectionWidget extends BaseWidget<SectionWidgetProps, WidgetState> {
getWidgetView(): ReactNode {
return (
<ContainerComponent {...this.props} noScroll>
<ContainerComponent
elevatedBackground={this.props.elevatedBackground}
elevation={Elevations.SECTION_ELEVATION}
{...this.props}
>
<LayoutProvider {...this.props} />
</ContainerComponent>
);

View File

@ -14,7 +14,7 @@ import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidg
import { getWidgetBluePrintUpdates } from "utils/WidgetBlueprintUtils";
export const defaultConfig: WidgetDefaultProps = {
backgroundColor: "ghostwhite",
elevatedBackground: true,
children: [],
columns: 0,
detachFromLayout: false,

View File

@ -2,64 +2,20 @@ import { ValidationTypes } from "constants/WidgetValidation";
export const propertyPaneStyle = [
{
sectionName: "Color",
sectionName: "Background",
children: [
{
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
propertyName: "backgroundColor",
label: "Background color",
controlType: "COLOR_PICKER",
propertyName: "elevatedBackground",
label: "Background",
controlType: "SWITCH",
fullWidth: true,
helpText: "Sets the semantic elevated background of the zone",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
propertyName: "borderColor",
label: "Border color",
controlType: "COLOR_PICKER",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
{
sectionName: "Border and shadow",
children: [
{
helpText: "Enter value for border width",
propertyName: "borderWidth",
label: "Border width",
placeholderText: "Enter value in px",
controlType: "INPUT_TEXT",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.NUMBER },
},
{
propertyName: "borderRadius",
label: "Border radius",
helpText: "Rounds the corners of the icon button's outer border edge",
controlType: "BORDER_RADIUS_OPTIONS",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "boxShadow",
label: "Box shadow",
helpText:
"Enables you to cast a drop shadow from the frame of the widget",
controlType: "BOX_SHADOW_OPTIONS",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
validation: {
type: ValidationTypes.BOOLEAN,
},
},
],
},

View File

@ -19,9 +19,9 @@ import BaseWidget from "widgets/BaseWidget";
import type { WidgetProps, WidgetState } from "widgets/BaseWidget";
import type { LayoutProps } from "layoutSystems/anvil/utils/anvilTypes";
import type { ContainerWidgetProps } from "widgets/ContainerWidget/widget";
import ContainerComponent from "widgets/ContainerWidget/component";
import { ContainerComponent } from "widgets/anvil/Container";
import { LayoutProvider } from "layoutSystems/anvil/layoutComponents/LayoutProvider";
import { anvilWidgets } from "widgets/anvil/constants";
import { Elevations, anvilWidgets } from "widgets/anvil/constants";
class ZoneWidget extends BaseWidget<ZoneWidgetProps, WidgetState> {
static type = anvilWidgets.ZONE_WIDGET;
@ -82,7 +82,11 @@ class ZoneWidget extends BaseWidget<ZoneWidgetProps, WidgetState> {
getWidgetView(): ReactNode {
return (
<ContainerComponent {...this.props} noScroll>
<ContainerComponent
elevatedBackground={this.props.elevatedBackground}
elevation={Elevations.ZONE_ELEVATION}
{...this.props}
>
<LayoutProvider {...this.props} />
</ContainerComponent>
);

View File

@ -2,3 +2,8 @@ export const anvilWidgets = {
SECTION_WIDGET: "SECTION_WIDGET",
ZONE_WIDGET: "ZONE_WIDGET",
};
export enum Elevations {
SECTION_ELEVATION = 1,
ZONE_ELEVATION = 2,
}

View File

@ -73,9 +73,6 @@ class WDSParagraphWidget extends BaseWidget<TextWidgetProps, WidgetState> {
isBold={this.props?.fontStyle?.includes("bold")}
isItalic={this.props?.fontStyle?.includes("italic")}
lineClamp={this.props.lineClamp ? this.props.lineClamp : undefined}
style={{
paddingBottom: "0.5rem",
}}
textAlign={this.props.textAlign}
title={this.props.lineClamp ? this.props.text : undefined}
variant={this.props.fontSize}

View File

@ -1181,8 +1181,6 @@ export class WDSTableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
};
getPaddingAdjustedDimensions = () => {
// eslint-disable-next-line prefer-const
let { componentHeight } = this.props;
// Hacky fix for now to supply width to table widget
let componentWidth: number =
document
@ -1190,7 +1188,7 @@ export class WDSTableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
?.getBoundingClientRect().width || this.props.componentWidth;
// (2 * WIDGET_PADDING) gives the total horizontal padding (i.e. paddingLeft + paddingRight)
componentWidth = componentWidth - 2 * WIDGET_PADDING;
return { componentHeight, componentWidth };
return { componentHeight: 300, componentWidth };
};
getWidgetView() {