chore: Add MutuationObserver to track changes in positions of widgets and layouts. (#28315)

This commit is contained in:
Preet Sidhu 2023-10-24 07:37:06 -04:00 committed by GitHub
parent c4057c2ea5
commit 159a26fb6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 271 additions and 64 deletions

View File

@ -1,3 +1,4 @@
export interface AdditionalAnvilProperties {
layoutId: string;
layoutId: string; // Id of nearest drop target ancestor layout.
rowIndex: number; // Index of the widget in it's parent layout.
}

View File

@ -105,8 +105,17 @@ export function AnvilFlexComponent(props: AnvilFlexComponentProps) {
props.widgetId
} ${widgetTypeClassname(
props.widgetType,
)} t--widget-${props.widgetName.toLowerCase()}`,
[props.parentId, props.widgetId, props.widgetType, props.widgetName],
)} t--widget-${props.widgetName.toLowerCase()} drop-target-${
props.layoutId
} row-index-${props.rowIndex}`,
[
props.parentId,
props.widgetId,
props.widgetType,
props.widgetName,
props.layoutId,
props.rowIndex,
],
);
// Memoize flex props to be passed to the WDS Flex component.

View File

@ -43,7 +43,9 @@ export const AnvilEditorWidgetOnion = (props: BaseWidgetProps) => {
return (
<AnvilFlexComponent
isResizeDisabled={props.resizeDisabled}
layoutId={props.layoutId}
parentId={props.parentId}
rowIndex={props.rowIndex}
widgetId={props.widgetId}
widgetName={props.widgetName}
widgetSize={widgetSize}

View File

@ -35,8 +35,14 @@ class AlignedLayoutColumn extends BaseLayoutComponent {
}
render() {
const { canvasId, isDropTarget, layoutId, layoutStyle, renderMode } =
this.props;
const {
canvasId,
isDropTarget,
layoutId,
layoutIndex,
layoutStyle,
renderMode,
} = this.props;
return (
<FlexLayout
@ -44,6 +50,7 @@ class AlignedLayoutColumn extends BaseLayoutComponent {
direction="column"
isDropTarget={!!isDropTarget}
layoutId={layoutId}
layoutIndex={layoutIndex}
renderMode={renderMode}
{...(layoutStyle || {})}
>

View File

@ -37,8 +37,14 @@ class AlignedWidgetColumn extends BaseLayoutComponent {
static rendersWidgets: boolean = true;
render() {
const { canvasId, isDropTarget, layoutId, layoutStyle, renderMode } =
this.props;
const {
canvasId,
isDropTarget,
layoutId,
layoutIndex,
layoutStyle,
renderMode,
} = this.props;
return (
<FlexLayout
@ -46,6 +52,7 @@ class AlignedWidgetColumn extends BaseLayoutComponent {
direction="column"
isDropTarget={!!isDropTarget}
layoutId={layoutId}
layoutIndex={layoutIndex}
renderMode={renderMode}
{...(layoutStyle || {})}
>

View File

@ -29,8 +29,14 @@ class AlignedWidgetRow extends BaseLayoutComponent {
static rendersWidgets: boolean = true;
render() {
const { canvasId, isDropTarget, layoutId, layoutStyle, renderMode } =
this.props;
const {
canvasId,
isDropTarget,
layoutId,
layoutIndex,
layoutStyle,
renderMode,
} = this.props;
return (
<FlexLayout
@ -40,6 +46,7 @@ class AlignedWidgetRow extends BaseLayoutComponent {
direction="row"
isDropTarget={!!isDropTarget}
layoutId={layoutId}
layoutIndex={layoutIndex}
renderMode={renderMode}
wrap="wrap"
{...(layoutStyle || {})}

View File

@ -31,6 +31,7 @@ export interface FlexLayoutProps
children: ReactNode;
isDropTarget?: boolean;
layoutId: string;
layoutIndex: number;
renderMode: RenderMode;
border?: string;
@ -66,6 +67,7 @@ export const FlexLayout = React.memo((props: FlexLayoutProps) => {
isDropTarget,
justifyContent,
layoutId,
layoutIndex,
maxHeight,
maxWidth,
minHeight,
@ -147,9 +149,14 @@ export const FlexLayout = React.memo((props: FlexLayoutProps) => {
};
}, [border, isDropTarget, position, renderMode]);
const className = useMemo(() => {
return `layout-${layoutId} layout-index-${layoutIndex}`;
}, [layoutId, layoutIndex]);
return (
<Flex
{...flexProps}
className={className}
id={getAnvilLayoutDOMId(canvasId, layoutId)}
ref={ref}
style={styleProps}

View File

@ -34,8 +34,14 @@ class LayoutColumn extends BaseLayoutComponent {
}
render() {
const { canvasId, isDropTarget, layoutId, layoutStyle, renderMode } =
this.props;
const {
canvasId,
isDropTarget,
layoutId,
layoutIndex,
layoutStyle,
renderMode,
} = this.props;
return (
<FlexLayout
@ -43,6 +49,7 @@ class LayoutColumn extends BaseLayoutComponent {
direction="column"
isDropTarget={!!isDropTarget}
layoutId={layoutId}
layoutIndex={layoutIndex}
renderMode={renderMode}
{...(layoutStyle || {})}
>

View File

@ -22,8 +22,14 @@ class LayoutRow extends BaseLayoutComponent {
static deriveHighlights: DeriveHighlightsFn = deriveRowHighlights;
render() {
const { canvasId, isDropTarget, layoutId, layoutStyle, renderMode } =
this.props;
const {
canvasId,
isDropTarget,
layoutId,
layoutIndex,
layoutStyle,
renderMode,
} = this.props;
return (
<FlexLayout
@ -33,6 +39,7 @@ class LayoutRow extends BaseLayoutComponent {
direction="row"
isDropTarget={!!isDropTarget}
layoutId={layoutId}
layoutIndex={layoutIndex}
renderMode={renderMode}
{...(layoutStyle || {})}
>

View File

@ -36,8 +36,14 @@ class WidgetColumn extends BaseLayoutComponent {
static rendersWidgets: boolean = true;
render() {
const { canvasId, isDropTarget, layoutId, layoutStyle, renderMode } =
this.props;
const {
canvasId,
isDropTarget,
layoutId,
layoutIndex,
layoutStyle,
renderMode,
} = this.props;
return (
<FlexLayout
@ -45,6 +51,7 @@ class WidgetColumn extends BaseLayoutComponent {
direction="column"
isDropTarget={!!isDropTarget}
layoutId={layoutId}
layoutIndex={layoutIndex}
renderMode={renderMode}
{...(layoutStyle || {})}
>

View File

@ -24,8 +24,14 @@ class WidgetRow extends BaseLayoutComponent {
static rendersWidgets: boolean = true;
render() {
const { canvasId, isDropTarget, layoutId, layoutStyle, renderMode } =
this.props;
const {
canvasId,
isDropTarget,
layoutId,
layoutIndex,
layoutStyle,
renderMode,
} = this.props;
return (
<FlexLayout
@ -35,6 +41,7 @@ class WidgetRow extends BaseLayoutComponent {
direction="row"
isDropTarget={!!isDropTarget}
layoutId={layoutId}
layoutIndex={layoutIndex}
renderMode={renderMode}
{...(layoutStyle || {})}
>

View File

@ -52,6 +52,7 @@ export interface LayoutComponentProps extends LayoutProps {
canvasId: string; // Parent canvas of the layout.
children?: React.ReactNode; // The children of the layout component
childrenMap: Record<string, WidgetProps>; // Map of child widget ids to their props.
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.
renderMode: RenderMode;

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { type ReactNode } from "react";
import LayoutFactory from "layoutSystems/anvil/layoutComponents/LayoutFactory";
import type {
LayoutComponentProps,
@ -7,25 +7,47 @@ import type {
} from "../anvilTypes";
import { type RenderMode, RenderModes } from "constants/WidgetConstants";
import { isFillWidgetPresentInList } from "./widgetUtils";
import { MOBILE_BREAKPOINT } from "../constants";
import { AlignmentIndexMap, MOBILE_BREAKPOINT } from "../constants";
import {
FlexLayout,
type FlexLayoutProps,
} from "layoutSystems/anvil/layoutComponents/components/FlexLayout";
import { FlexLayerAlignment } from "layoutSystems/common/utils/constants";
import { isWidgetLayoutProps } from "./typeUtils";
import { renderChildren } from "layoutSystems/common/utils/canvasUtils";
import { renderChildWidget } from "layoutSystems/common/utils/canvasUtils";
import type BaseLayoutComponent from "layoutSystems/anvil/layoutComponents/BaseLayoutComponent";
import type { WidgetProps } from "widgets/BaseWidget";
export function renderWidgets(props: LayoutComponentProps) {
/**
*
* @param props | LayoutComponentProps : Component properties of a layout.
* @param startIndex | number (optional) : The index of the first child.
* @returns ReactNode[] | List of rendered child widgets
*/
export function renderWidgets(
props: LayoutComponentProps,
startIndex = 0,
): ReactNode[] {
const { canvasId, childrenMap, parentDropTarget, renderMode } = props;
return renderChildren(
Object.values(childrenMap).filter((each) => !!each),
canvasId,
renderMode as RenderModes,
{},
{ layoutId: parentDropTarget },
);
/**
* startIndex is needed because AlignedWidgetRow uses three child Rows to render it's widgets.
* startIndex is used to correctly determine the index of a widget in the layout.
*/
return Object.values(childrenMap)
.filter((each) => !!each)
.map((each: WidgetProps, index: number) => {
return renderChildWidget({
childWidgetData: each,
defaultWidgetProps: {},
layoutSystemProps: {
layoutId: parentDropTarget,
rowIndex: index + startIndex,
},
noPad: false,
renderMode: renderMode as RenderModes,
widgetId: canvasId,
});
});
}
/**
@ -42,7 +64,7 @@ export function renderLayouts(
renderMode: RenderMode = RenderModes.CANVAS,
layoutOrder: string[],
): JSX.Element[] {
return layouts.map((layout) => {
return layouts.map((layout: LayoutProps, index: number) => {
const Component: typeof BaseLayoutComponent = LayoutFactory.get(
layout.layoutType,
);
@ -52,6 +74,7 @@ export function renderLayouts(
canvasId={canvasId}
childrenMap={getChildrenMap(layout, childrenMap)}
key={layout.layoutId}
layoutIndex={index}
layoutOrder={layoutOrder}
parentDropTarget={
layout.isDropTarget ? layout.layoutId : parentDropTarget
@ -134,7 +157,10 @@ export function renderWidgetsInAlignedRow(
* else render the child widgets separately
* in their respective alignments.
*/
const commonProps: Omit<FlexLayoutProps, "children" | "layoutId"> = {
const commonProps: Omit<
FlexLayoutProps,
"children" | "layoutId" | "layoutIndex"
> = {
alignSelf: "stretch",
canvasId,
columnGap: "4px",
@ -146,6 +172,17 @@ export function renderWidgetsInAlignedRow(
wrap: { base: "wrap", [`${MOBILE_BREAKPOINT}px`]: "nowrap" },
};
const startChildren: Record<string, WidgetProps> = getChildrenMapForAlignment(
props,
FlexLayerAlignment.Start,
);
const centerChildren: Record<string, WidgetProps> =
getChildrenMapForAlignment(props, FlexLayerAlignment.Center);
const endChildren: Record<string, WidgetProps> = getChildrenMapForAlignment(
props,
FlexLayerAlignment.End,
);
// TODO: After positionObserver integration,
// check if use of FlexLayout is causing performance or other issues.
// WDS Flex can be used as a replacement.
@ -153,41 +190,45 @@ export function renderWidgetsInAlignedRow(
<FlexLayout
{...commonProps}
justifyContent="start"
key={`${layoutId}-0`}
layoutId={`${layoutId}-0`}
key={`${layoutId}-${AlignmentIndexMap[FlexLayerAlignment.Start]}`}
layoutId={`${layoutId}-${AlignmentIndexMap[FlexLayerAlignment.Start]}`}
layoutIndex={AlignmentIndexMap[FlexLayerAlignment.Start]}
>
{renderWidgets({
...props,
childrenMap: getChildrenMapForAlignment(
props,
FlexLayerAlignment.Start,
),
childrenMap: startChildren,
})}
</FlexLayout>,
<FlexLayout
{...commonProps}
justifyContent="center"
key={`${layoutId}-1`}
layoutId={`${layoutId}-1`}
key={`${layoutId}-${AlignmentIndexMap[FlexLayerAlignment.Center]}`}
layoutId={`${layoutId}-${AlignmentIndexMap[FlexLayerAlignment.Center]}`}
layoutIndex={AlignmentIndexMap[FlexLayerAlignment.Start]}
>
{renderWidgets({
...props,
childrenMap: getChildrenMapForAlignment(
props,
FlexLayerAlignment.Center,
),
})}
{renderWidgets(
{
...props,
childrenMap: centerChildren,
},
Object.keys(startChildren)?.length,
)}
</FlexLayout>,
<FlexLayout
{...commonProps}
justifyContent="end"
key={`${layoutId}-2`}
layoutId={`${layoutId}-2`}
key={`${layoutId}-${AlignmentIndexMap[FlexLayerAlignment.End]}`}
layoutId={`${layoutId}-${AlignmentIndexMap[FlexLayerAlignment.End]}`}
layoutIndex={AlignmentIndexMap[FlexLayerAlignment.End]}
>
{renderWidgets({
...props,
childrenMap: getChildrenMapForAlignment(props, FlexLayerAlignment.End),
})}
{renderWidgets(
{
...props,
childrenMap: endChildren,
},
Object.keys(startChildren)?.length +
Object.keys(centerChildren)?.length,
)}
</FlexLayout>,
];
}

View File

@ -5,8 +5,10 @@ import type { WidgetType } from "WidgetProvider/factory";
export interface AnvilFlexComponentProps {
children: ReactNode;
isResizeDisabled?: boolean;
layoutId: string;
focused?: boolean;
parentId?: string;
rowIndex: number;
selected?: boolean;
widgetId: string;
widgetName: string;

View File

@ -25,7 +25,9 @@ export const AnvilViewerWidgetOnion = (props: BaseWidgetProps) => {
return (
<AnvilFlexComponent
isResizeDisabled={props.resizeDisabled}
layoutId={props.layoutId}
parentId={props.parentId}
rowIndex={props.rowIndex}
widgetId={props.widgetId}
widgetName={props.widgetName}
widgetSize={widgetSize}

View File

@ -22,13 +22,7 @@ class LayoutElementPositionObserver {
private registeredWidgets: {
[widgetDOMId: string]: { ref: RefObject<HTMLDivElement>; id: string };
} = {};
private registeredLayers: {
[layerId: string]: {
ref: RefObject<HTMLDivElement>;
canvasId: string;
layerIndex: number;
};
} = {};
private registeredLayouts: {
[layoutDOMId: string]: {
ref: RefObject<HTMLDivElement>;
@ -38,6 +32,11 @@ class LayoutElementPositionObserver {
};
} = {};
private mutationOptions: MutationObserverInit = {
attributes: true,
attributeFilter: ["class"],
};
private debouncedProcessBatch = debounce(this.processWidgetBatch, 200);
// All the registered elements are registered with this Resize observer
@ -48,10 +47,22 @@ class LayoutElementPositionObserver {
for (const entry of entries) {
if (entry?.target?.id) {
const DOMId = entry?.target?.id;
if (DOMId.indexOf(ANVIL_WIDGET) > -1) {
this.addWidgetToProcess(DOMId);
} else if (DOMId.indexOf(LAYOUT) > -1) {
this.addLayoutToProcess(DOMId);
this.trackEntry(DOMId);
}
}
},
);
private mutationObserver = new MutationObserver(
(mutations: MutationRecord[]) => {
for (const mutation of mutations) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "class"
) {
const DOMId: string = (mutation?.target as HTMLElement)?.id;
if (DOMId) {
this.trackEntry(DOMId);
}
}
}
@ -64,6 +75,7 @@ class LayoutElementPositionObserver {
const widgetDOMId = getAnvilWidgetDOMId(widgetId);
this.registeredWidgets[widgetDOMId] = { ref, id: widgetId };
this.resizeObserver.observe(ref.current);
this.mutationObserver.observe(ref.current, this.mutationOptions);
}
}
@ -94,6 +106,7 @@ class LayoutElementPositionObserver {
isDropTarget,
};
this.resizeObserver.observe(ref.current);
this.mutationObserver.observe(ref.current, this.mutationOptions);
}
}
@ -144,6 +157,14 @@ class LayoutElementPositionObserver {
public getRegisteredLayouts() {
return this.registeredLayouts;
}
private trackEntry(DOMId: string) {
if (DOMId.indexOf(ANVIL_WIDGET) > -1) {
this.addWidgetToProcess(DOMId);
} else if (DOMId.indexOf(LAYOUT) > -1) {
this.addLayoutToProcess(DOMId);
}
}
}
export const positionObserver = new LayoutElementPositionObserver();

View File

@ -19,7 +19,7 @@ type LayoutSystemProps =
*
* @returns
*/
function renderChildWidget({
export function renderChildWidget({
childWidgetData,
defaultWidgetProps,
layoutSystemProps,

View File

@ -47,6 +47,7 @@ export function generateLayoutComponentMock(
return {
layout,
layoutId: generateReactKey(),
layoutIndex: 0,
layoutStyle: {},
layoutType: type,
@ -98,6 +99,7 @@ export function generateAlignedRowMock(
return {
layout,
layoutId: "",
layoutIndex: 0,
layoutStyle: {},
layoutType: LayoutComponentTypes.ALIGNED_WIDGET_ROW,

View File

@ -10,7 +10,10 @@ import type { BaseInputWidgetProps } from "./types";
import { propertyPaneContentConfig } from "./contentConfig";
import { FILL_WIDGET_MIN_WIDTH } from "constants/minWidthConstants";
import { ResponsiveBehavior } from "layoutSystems/common/utils/constants";
import type { WidgetBaseConfiguration } from "WidgetProvider/constants";
import type {
AnvilConfig,
WidgetBaseConfiguration,
} from "WidgetProvider/constants";
class WDSBaseInputWidget<
T extends BaseInputWidgetProps,
@ -81,6 +84,17 @@ class WDSBaseInputWidget<
};
}
static getAnvilConfig(): AnvilConfig | null {
return {
widgetSize: {
maxHeight: {},
maxWidth: {},
minHeight: { base: "70px" },
minWidth: { base: "120px" },
},
};
}
/**
* disabled drag on focusState: true
*

View File

@ -0,0 +1,10 @@
import type { AnvilConfig } from "WidgetProvider/constants";
export const anvilConfig: AnvilConfig = {
widgetSize: {
maxHeight: {},
maxWidth: {},
minHeight: { base: "40px" },
minWidth: { base: "120px" },
},
};

View File

@ -5,3 +5,4 @@ export { methodsConfig } from "./methodsConfig";
export { defaultsConfig } from "./defaultsConfig";
export { featuresConfig } from "./featuresConfig";
export { autocompleteConfig } from "./autocompleteConfig";
export { anvilConfig } from "./anvilConfig";

View File

@ -8,6 +8,7 @@ import * as config from "./../config";
import BaseWidget from "widgets/BaseWidget";
import type { CheckboxWidgetProps } from "./types";
import type { WidgetState } from "widgets/BaseWidget";
import type { AnvilConfig } from "WidgetProvider/constants";
class WDSCheckboxWidget extends BaseWidget<CheckboxWidgetProps, WidgetState> {
static type = "WDS_CHECKBOX_WIDGET";
@ -32,6 +33,10 @@ class WDSCheckboxWidget extends BaseWidget<CheckboxWidgetProps, WidgetState> {
return {};
}
static getAnvilConfig(): AnvilConfig | null {
return config.anvilConfig;
}
static getAutocompleteDefinitions() {
return config.autocompleteConfig;
}

View File

@ -12,6 +12,7 @@ import {
} from "./../config";
import type { IconButtonWidgetProps, IconButtonWidgetState } from "./types";
import { IconButtonComponent } from "../component";
import type { AnvilConfig } from "WidgetProvider/constants";
class WDSIconButtonWidget extends BaseWidget<
IconButtonWidgetProps,
@ -39,6 +40,17 @@ class WDSIconButtonWidget extends BaseWidget<
return {};
}
static getAnvilConfig(): AnvilConfig | null {
return {
widgetSize: {
maxHeight: {},
maxWidth: {},
minHeight: { base: "40px" },
minWidth: { base: "40px" },
},
};
}
static getAutocompleteDefinitions() {
return autocompleteConfig;
}

View File

@ -122,6 +122,7 @@ import { getMemoiseTransformDataWithEditableCell } from "./reactTableUtils/trans
import type { ExtraDef } from "utils/autocomplete/defCreatorUtils";
import { generateTypeDef } from "utils/autocomplete/defCreatorUtils";
import type {
AnvilConfig,
AutocompletionDefinitions,
PropertyUpdates,
SnipingModeProperty,
@ -373,6 +374,17 @@ export class WDSTableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
};
}
static getAnvilConfig(): AnvilConfig | null {
return {
widgetSize: {
maxHeight: {},
maxWidth: {},
minHeight: { base: "300px" },
minWidth: { base: "280px" },
},
};
}
static getPropertyPaneContentConfig() {
return contentConfig;
}

View File

@ -0,0 +1,10 @@
import type { AnvilConfig } from "WidgetProvider/constants";
export const anvilConfig: AnvilConfig = {
widgetSize: {
maxHeight: {},
maxWidth: {},
minHeight: { base: "40px" },
minWidth: { base: "120px" },
},
};

View File

@ -5,3 +5,4 @@ export { methodsConfig } from "./methodsConfig";
export { defaultsConfig } from "./defaultsConfig";
export { featuresConfig } from "./featuresConfig";
export { autocompleteConfig } from "./autocompleteConfig";
export { anvilConfig } from "./anvilConfig";

View File

@ -7,6 +7,7 @@ import BaseWidget from "widgets/BaseWidget";
import { Text } from "@design-system/widgets";
import type { TextWidgetProps } from "./types";
import type { WidgetState } from "widgets/BaseWidget";
import type { AnvilConfig } from "WidgetProvider/constants";
class WDSTextWidget extends BaseWidget<TextWidgetProps, WidgetState> {
static type = "WDS_TEXT_WIDGET";
@ -31,6 +32,10 @@ class WDSTextWidget extends BaseWidget<TextWidgetProps, WidgetState> {
return {};
}
static getAnvilConfig(): AnvilConfig | null {
return config.anvilConfig;
}
static getAutocompleteDefinitions() {
return config.autocompleteConfig;
}