Repository: maxGraph/maxGraph
Pull request #409 (2024-05-22 12:14:40)

fix: correctly resolve 'none' values of CellStyle properties
The 'none' values were previously equivalent to 'undefined' values, so the values fallback to the default or baseStyle values, instead of not being set. The fix restore the documented behavior.

Also introduce
  - a new type for CellStyle color properties that support placeholders
  - a new Story which demonstrates the usage of 'none' and placeholders

## Notes

Detected in #406
Bug introduced in #383, due to a misinterpretation of the former mxGraph implementation.

Original mxGraph's behavior:
- documentation: https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/view/mxStylesheet.js#L42-L43
- code: https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/view/mxStylesheet.js#L236-L239

## New story

![Styles _ ColorStylePlaceHolders - Default ⋅ Storybook](https://github.com/maxGraph/maxGraph/assets/27200110/c8f743bb-9cda-45a6-925a-e9ac8373ef9c)




Patch:
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 362c86e94..9718ae826 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -197,7 +197,7 @@ export type CellStateStyle = {
   endFill?: boolean;
   /**
    * Set the fill color of the end arrow marker if {@link endFill} is `true`.
-   * If not set, use {@link startStrokeColor}.
+   * If not set, use {@link strokeColor}.
    * @since 0.10.0
    */
   endFillColor?: ColorValue;
@@ -257,11 +257,13 @@ export type CellStateStyle = {
    */
   exitY?: number;
   /**
-   * The possible values are all HTML color names or HEX codes, as well as special keywords such
-   * as `swimlane`, `inherit`, `indicated` to use the color code of a related cell or the
-   * indicator shape.
+   * The possible values are all HTML color names or HEX codes, as well as special keywords such as:
+   * - `indicated` to use the color of a related cell or the indicator shape
+   * - `inherit` to use the color of the direct parent cell
+   * - `none` for no color
+   * - `swimlane` to use the color of the parent swimlane if one exists in the parent hierarchy
    */
-  fillColor?: ColorValue;
+  fillColor?: SpecialStyleColorValue;
   /**
    * Possible range is `0-100`.
    */
@@ -290,9 +292,13 @@ export type CellStateStyle = {
    */
   foldable?: boolean;
   /**
-   * The possible values are all HTML color names or HEX codes.
+   * The possible values are all HTML color names or HEX codes, as well as special keywords such as:
+   * - `indicated` to use the color of a related cell or the indicator shape
+   * - `inherit` to use the color of the direct parent cell
+   * - `none` for no color
+   * - `swimlane` to use the color of the parent swimlane if one exists in the parent hierarchy
    */
-  fontColor?: ColorValue;
+  fontColor?: SpecialStyleColorValue;
   /**
    * The possible values are names such as `Arial; Dialog; Verdana; Times New Roman`.
    */
@@ -312,9 +318,13 @@ export type CellStateStyle = {
    */
   glass?: boolean;
   /**
-   * The possible values are all HTML color names or HEX codes.
+   * The possible values are all HTML color names or HEX codes, as well as special keywords such as:
+   * - `indicated` to use the color of a related cell or the indicator shape
+   * - `inherit` to use the color of the direct parent cell
+   * - `none` for no color
+   * - `swimlane` to use the color of the parent swimlane if one exists in the parent hierarchy
    */
-  gradientColor?: ColorValue;
+  gradientColor?: SpecialStyleColorValue;
   /**
    * Generally, and by default in maxGraph, gradient painting is done from the value of {@link fillColor} to the value of {@link gradientColor}.
    * If we take the example of 'north', this means that the {@link fillColor} color is at the bottom of paint pattern
@@ -733,11 +743,13 @@ export type CellStateStyle = {
    */
   startSize?: number;
   /**
-   * The possible values are all HTML color names or HEX codes, as well as special keywords such
-   * as `swimlane`, `inherit`, `indicated` to use the color code of a related cell or the
-   * indicator shape or `none` for no color.
+   * The possible values are all HTML color names or HEX codes, as well as special keywords such as:
+   * - `indicated` to use the color of a related cell or the indicator shape
+   * - `inherit` to use the color of the direct parent cell
+   * - `none` for no color
+   * - `swimlane` to use the color of the parent swimlane if one exists in the parent hierarchy
    */
-  strokeColor?: ColorValue;
+  strokeColor?: SpecialStyleColorValue;
   /**
    * The possible range is `0-100`.
    */
@@ -752,7 +764,7 @@ export type CellStateStyle = {
   /**
    * The fill color of the `swimlane` background.
    * The possible values are all HTML color names or HEX codes.
-   * @default no backgroune
+   * @default no background
    */
   swimlaneFillColor?: ColorValue;
   /**
@@ -841,6 +853,14 @@ export type NumericCellStateStyleKeys = NonNullable<
 >;
 
 export type ColorValue = string;
+/** Color values and special placeholders used to resolve colors (see {@link CellRenderer.resolveColor}) for style properties. */
+export type SpecialStyleColorValue =
+  | 'indicated'
+  | 'inherit'
+  | 'none'
+  | 'swimlane'
+  | (string & {});
+
 export type DirectionValue = 'north' | 'south' | 'east' | 'west';
 export type TextDirectionValue = '' | 'ltr' | 'rtl' | 'auto';
 export type AlignValue = 'left' | 'center' | 'right';
diff --git a/packages/core/src/view/handler/ConnectionHandler.ts b/packages/core/src/view/handler/ConnectionHandler.ts
index 7b4f4dba1..aaffc9898 100644
--- a/packages/core/src/view/handler/ConnectionHandler.ts
+++ b/packages/core/src/view/handler/ConnectionHandler.ts
@@ -534,7 +534,7 @@ class ConnectionHandler extends EventSource implements GraphPlugin {
   /**
    * Starts a new connection for the given state and coordinates.
    */
-  start(state: CellState, x: number, y: number, edgeState: CellState) {
+  start(state: CellState, x: number, y: number, edgeState?: CellState) {
     this.previous = state;
     this.first = new Point(x, y);
     this.edgeState = edgeState ?? this.createEdgeState();
@@ -1806,7 +1806,10 @@ class ConnectionHandler extends EventSource implements GraphPlugin {
         }
       } catch (e) {
         MaxLog.show();
-        // MaxLog.debug(e.message);
+        const errorMessage = `Error in ConnectionHandler: ${
+          e instanceof Error ? e.message + '\n' + e.stack : 'unknown cause'
+        }`;
+        MaxLog.debug(errorMessage);
       } finally {
         model.endUpdate();
       }
diff --git a/packages/core/src/view/mixins/CellsMixin.ts b/packages/core/src/view/mixins/CellsMixin.ts
index 87b2ed2a3..28c08680c 100644
--- a/packages/core/src/view/mixins/CellsMixin.ts
+++ b/packages/core/src/view/mixins/CellsMixin.ts
@@ -880,20 +880,20 @@ export const CellsMixin: PartialType = {
   /**
    * Returns the clone for the given cell. Uses {@link cloneCells}.
    *
-   * @param cell {@link mxCell} to be cloned.
+   * @param cell {@link Cell} to be cloned.
    * @param allowInvalidEdges Optional boolean that specifies if invalid edges
    * should be cloned. Default is `true`.
    * @param mapping Optional mapping for existing clones.
    * @param keepPosition Optional boolean indicating if the position of the cells should
    * be updated to reflect the lost parent cell. Default is `false`.
    */
-  cloneCell(cell, allowInvalidEdges = false, mapping = null, keepPosition = false) {
+  cloneCell(cell, allowInvalidEdges = false, mapping = {}, keepPosition = false) {
     return this.cloneCells([cell], allowInvalidEdges, mapping, keepPosition)[0];
   },
 
   /**
    * Returns the clones for the given cells. The clones are created recursively
-   * using {@link mxGraphModel.cloneCells}. If the terminal of an edge is not in the
+   * using {@link cloneCells}. If the terminal of an edge is not in the
    * given array, then the respective end is assigned a terminal point and the
    * terminal is removed.
    *
diff --git a/packages/core/src/view/style/Stylesheet.ts b/packages/core/src/view/style/Stylesheet.ts
index 01f01e877..ce52c644b 100644
--- a/packages/core/src/view/style/Stylesheet.ts
+++ b/packages/core/src/view/style/Stylesheet.ts
@@ -164,6 +164,9 @@ export class Stylesheet {
    *   - registered styles referenced in `baseStyleNames`, in the order of the array
    *   - `cellStyle` parameter
    *
+   * To fully unset a style property i.e. the property is not set even if a value is set in the default style or in the referenced styles,
+   * set the `cellStyle` property to `none`. For example. `cellStyle.fillColor = 'none'`
+   *
    * @param cellStyle An object that represents the style.
    * @param defaultStyle Default style used as reference to compute the returned style.
    */
@@ -186,15 +189,13 @@ export class Stylesheet {
     }
 
     // Merges cellStyle into style
-    const filteredCellStyle = clone(cellStyle); // Clones the cellStyle to avoid modifying the original object pass as parameter
-    for (const key of Object.keys(filteredCellStyle)) {
-      (filteredCellStyle[key] === undefined || filteredCellStyle[key] == NONE) &&
-        delete filteredCellStyle[key];
+    for (const key of Object.keys(cellStyle)) {
+      // @ts-ignore
+      if (cellStyle[key] !== undefined) {
+        // @ts-ignore
+        cellStyle[key] == NONE ? delete style[key] : (style[key] = cellStyle[key]);
+      }
     }
-    style = {
-      ...style,
-      ...filteredCellStyle,
-    };
 
     // Remove the 'baseStyleNames' that may have been copied from the cellStyle parameter to match the method signature
     'baseStyleNames' in style && delete style.baseStyleNames;
diff --git a/packages/html/stories/Collapse.stories.js b/packages/html/stories/Collapse.stories.ts
similarity index 66%
rename from packages/html/stories/Collapse.stories.js
rename to packages/html/stories/Collapse.stories.ts
index 3f5ac2766..bea1c89d1 100644
--- a/packages/html/stories/Collapse.stories.js
+++ b/packages/html/stories/Collapse.stories.ts
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { Graph, Rectangle } from '@maxgraph/core';
+import { type Cell, type CellStyle, Graph, Rectangle } from '@maxgraph/core';
 
 import { globalTypes, globalValues } from './shared/args.js';
 import {
@@ -34,7 +34,7 @@ export default {
   },
 };
 
-const Template = ({ label, ...args }) => {
+const Template = ({ label, ...args }: Record<string, string>) => {
   configureImagesBasePath();
   const container = createGraphContainer(args);
 
@@ -42,22 +42,21 @@ const Template = ({ label, ...args }) => {
   const parent = graph.getDefaultParent();
   configureExpandedAndCollapsedImages(graph);
 
-  const getStyle = function () {
-    // Extends Transactions.getStyle to show an image when collapsed
-    // TODO cannot use super without a parent class
-    // let style = super.getStyle();
-    let style = '';
-    if (this.isCollapsed()) {
-      style = {
-        ...style,
-        shape: 'image',
-        image: 'http://www.jgraph.com/images/mxgraph.gif',
-        noLabel: 1,
-        imageBackground: '#C3D9FF',
-        imageBorder: '#6482B9',
-      };
+  // Extends Cell.getStyle to show an image when collapsed
+  const getStyle = function (this: Cell): CellStyle {
+    if (!this.isCollapsed()) {
+      return this.style;
     }
-    return style;
+    return {
+      // Need to create a copy the original style as we don't want to change the original style stored in the Cell
+      // Otherwise, when expanding the cell, the style will be incorrect
+      ...this.style,
+      shape: 'image',
+      image: './images/package.png',
+      noLabel: true,
+      imageBackground: '#e7e9ef',
+      imageBorder: '#6482B9',
+    };
   };
 
   graph.batchUpdate(() => {
@@ -68,8 +67,8 @@ const Template = ({ label, ...args }) => {
       size: [200, 200],
       style: { shape: 'swimlane', startSize: 20 },
     });
-    v1.geometry.alternateBounds = new Rectangle(0, 0, 110, 70);
-    // v1.getStyle = getStyle;
+    v1.geometry!.alternateBounds = new Rectangle(0, 0, 110, 70);
+    v1.getStyle = getStyle;
 
     const v11 = graph.insertVertex({
       parent: v1,
@@ -77,7 +76,7 @@ const Template = ({ label, ...args }) => {
       position: [10, 40],
       size: [120, 80],
     });
-    // v11.getStyle = getStyle;
+    v11.getStyle = getStyle;
   });
 
   return container;
diff --git a/packages/html/stories/ColorStylePlaceHolders.stories.ts b/packages/html/stories/ColorStylePlaceHolders.stories.ts
new file mode 100644
index 000000000..a1d1d1611
--- /dev/null
+++ b/packages/html/stories/ColorStylePlaceHolders.stories.ts
@@ -0,0 +1,123 @@
+/*
+Copyright 2024-present The maxGraph project Contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { constants, Graph } from '@maxgraph/core';
+import { globalTypes, globalValues } from './shared/args.js';
+import { configureImagesBasePath, createGraphContainer } from './shared/configure.js';
+
+export default {
+  title: 'Styles/ColorStylePlaceHolders',
+  argTypes: {
+    ...globalTypes,
+  },
+  args: {
+    ...globalValues,
+  },
+};
+
+const Template = ({ label, ...args }: Record<string, string>) => {
+  configureImagesBasePath();
+  const container = createGraphContainer(args);
+
+  // Creates the graph inside the given container
+  const graph = new Graph(container);
+
+  // Disables global features
+  graph.cellsSelectable = false;
+  graph.cellsLocked = true;
+
+  // Sets global styles
+  const defaultVertexStyle = graph.getStylesheet().getDefaultVertexStyle();
+  defaultVertexStyle.foldable = false;
+
+  graph.getStylesheet().putCellStyle('swimlane', {
+    shape: constants.SHAPE.SWIMLANE,
+    startSize: 30,
+    fillColor: '#ffffff',
+    strokeColor: 'red',
+    swimlaneLine: true,
+    swimlaneFillColor: '#ffffff',
+  });
+
+  // Gets the default parent for inserting new cells. This
+  // is normally the first child of the root (ie. layer 0).
+  const parent = graph.getDefaultParent();
+
+  // Adds cells to the model in a single step
+  graph.batchUpdate(() => {
+    const swimlane = graph.insertVertex(parent, null, 'swimlane', 0, 0, 640, 400, {
+      baseStyleNames: ['swimlane'],
+    });
+
+    const parentInSwimlane = graph.insertVertex({
+      parent: swimlane,
+      value: 'Parent in swimlane',
+      position: [20, 80],
+      size: [400, 230],
+      style: { fillColor: '#e8dbdb', strokeColor: 'black' },
+    });
+
+    graph.insertVertex({
+      parent: parentInSwimlane,
+      value: 'child with none stroke color',
+      position: [10, 10],
+      size: [180, 30],
+      style: { strokeColor: 'none' },
+    });
+
+    graph.insertVertex({
+      parent: parentInSwimlane,
+      value: 'child with swimlane stroke color',
+      position: [205, 60],
+      size: [180, 30],
+      style: { strokeColor: 'swimlane' },
+    });
+    graph.insertVertex({
+      parent: parentInSwimlane,
+      value: 'child with swimlane fill color',
+      position: [10, 60],
+      size: [180, 30],
+      style: { fillColor: 'swimlane' },
+    });
+
+    graph.insertVertex({
+      parent: parentInSwimlane,
+      value: 'child with inherit stroke color',
+      position: [10, 160],
+      size: [180, 30],
+      style: { strokeColor: 'inherit' },
+    });
+    graph.insertVertex({
+      parent: parentInSwimlane,
+      value: 'child with inherit fill color',
+      position: [205, 160],
+      size: [180, 30],
+      style: { fillColor: 'inherit' },
+    });
+
+    graph.insertVertex({
+      // parent: swimlane,
+      value: 'vertex with none fill color',
+      position: [10, 410],
+      size: [180, 30],
+      style: { fillColor: 'none' },
+    });
+  });
+
+  return container;
+};
+
+export const Default = Template.bind({});
diff --git a/packages/html/stories/ContextIcons.stories.js b/packages/html/stories/ContextIcons.stories.ts
similarity index 79%
rename from packages/html/stories/ContextIcons.stories.js
rename to packages/html/stories/ContextIcons.stories.ts
index dcbc61f4e..079cbeced 100644
--- a/packages/html/stories/ContextIcons.stories.js
+++ b/packages/html/stories/ContextIcons.stories.ts
@@ -16,6 +16,7 @@ limitations under the License.
 */
 
 import {
+  Client,
   Graph,
   InternalEvent,
   RubberBandHandler,
@@ -23,6 +24,9 @@ import {
   styleUtils,
   domUtils,
   VertexHandler,
+  type CellState,
+  type ConnectionHandler,
+  type SelectionHandler,
 } from '@maxgraph/core';
 
 import {
@@ -31,7 +35,7 @@ import {
   rubberBandTypes,
   rubberBandValues,
 } from './shared/args.js';
-import { createGraphContainer } from './shared/configure.js';
+import { configureImagesBasePath, createGraphContainer } from './shared/configure.js';
 // style required by RubberBand
 import '@maxgraph/core/css/common.css';
 
@@ -47,18 +51,25 @@ export default {
   },
 };
 
-const Template = ({ label, ...args }) => {
+const Template = ({ label, ...args }: Record<string, any>) => {
+  configureImagesBasePath();
   const container = createGraphContainer(args);
 
-  class mxVertexToolHandler extends VertexHandler {
-    // Defines a subclass for VertexHandler that adds a set of clickable
-    // icons to every selected vertex.
+  // convenient function to create an image element available when the demo is not deployed in the root context
+  const createImage = (fileName: string) => {
+    return domUtils.createImage(`${Client.imageBasePath}/${fileName}`);
+  };
 
-    domNode = null;
+  // Defines a subclass for VertexHandler that adds a set of clickable icons to every selected vertex.
+  class CustomVertexToolHandler extends VertexHandler {
+    private domNode: HTMLDivElement = null!;
 
-    init() {
-      super.init();
+    constructor(state: CellState) {
+      super(state);
+      this.init();
+    }
 
+    init() {
       // In this example we force the use of DIVs for images in IE. This
       // handles transparency in PNG images properly in IE and fixes the
       // problem that IE routes all mouse events for a gesture via the
@@ -67,13 +78,8 @@ const Template = ({ label, ...args }) => {
       this.domNode.style.position = 'absolute';
       this.domNode.style.whiteSpace = 'nowrap';
 
-      // Workaround for event redirection via image tag in quirks and IE8
-      const createImage = (src) => {
-        return domUtils.createImage(src);
-      };
-
       // Delete
-      let img = createImage('images/delete2.png');
+      let img = createImage('delete2.png');
       img.setAttribute('title', 'Delete');
       img.style.cursor = 'pointer';
       img.style.width = '16px';
@@ -82,14 +88,14 @@ const Template = ({ label, ...args }) => {
         // Disables dragging the image
         InternalEvent.consume(evt);
       });
-      InternalEvent.addListener(img, 'click', (evt) => {
+      InternalEvent.addListener(img, 'click', (evt: Event) => {
         this.graph.removeCells([this.state.cell]);
         InternalEvent.consume(evt);
       });
       this.domNode.appendChild(img);
 
       // Size
-      img = createImage('images/fit_to_size.png');
+      img = createImage('fit_to_size.png');
       img.setAttribute('title', 'Resize');
       img.style.cursor = 'se-resize';
       img.style.width = '16px';
@@ -104,14 +110,14 @@ const Template = ({ label, ...args }) => {
       this.domNode.appendChild(img);
 
       // Move
-      img = createImage('images/plus.png');
+      img = createImage('plus.png');
       img.setAttribute('title', 'Move');
       img.style.cursor = 'move';
       img.style.width = '16px';
       img.style.height = '16px';
 
-      const graphHandler = graph.getPlugin('SelectionHandler');
-      const connectionHandler = graph.getPlugin('ConnectionHandler');
+      const graphHandler = graph.getPlugin('SelectionHandler') as SelectionHandler;
+      const connectionHandler = graph.getPlugin('ConnectionHandler') as ConnectionHandler;
 
       InternalEvent.addGestureListeners(img, (evt) => {
         graphHandler.start(
@@ -127,7 +133,7 @@ const Template = ({ label, ...args }) => {
       this.domNode.appendChild(img);
 
       // Connect
-      img = createImage('images/check.png');
+      img = createImage('check.png');
       img.setAttribute('title', 'Connect');
       img.style.cursor = 'pointer';
       img.style.width = '16px';
@@ -163,20 +169,20 @@ const Template = ({ label, ...args }) => {
       }
     }
 
-    destroy(sender, me) {
-      super.destroy(sender, me);
+    onDestroy() {
+      super.onDestroy();
 
-      if (this.domNode != null) {
-        this.domNode.parentNode.removeChild(this.domNode);
-        this.domNode = null;
+      if (this.domNode) {
+        this.domNode.parentNode?.removeChild(this.domNode);
+        this.domNode = null!;
       }
     }
   }
 
   class MyCustomGraph extends Graph {
-    createHandler(state) {
+    createHandler(state: CellState) {
       if (state != null && state.cell.isVertex()) {
-        return new mxVertexToolHandler(state);
+        return new CustomVertexToolHandler(state);
       }
       return super.createHandler(state);
     }
@@ -186,7 +192,7 @@ const Template = ({ label, ...args }) => {
   const graph = new MyCustomGraph(container);
   graph.setConnectable(true);
 
-  const connectionHandler = graph.getPlugin('ConnectionHandler');
+  const connectionHandler = graph.getPlugin('ConnectionHandler') as ConnectionHandler;
   connectionHandler.createTarget = true;
 
   // Uncomment the following if you want the container
@@ -214,7 +220,7 @@ const Template = ({ label, ...args }) => {
       position: [200, 150],
       size: [80, 30],
     });
-    const e1 = graph.insertEdge({
+    graph.insertEdge({
       parent,
       source: v1,
       target: v2,
diff --git a/packages/html/stories/SwimLanes.stories.js b/packages/html/stories/SwimLanes.stories.ts
similarity index 86%
rename from packages/html/stories/SwimLanes.stories.js
rename to packages/html/stories/SwimLanes.stories.ts
index 8aa37076b..0a6bbf1ad 100644
--- a/packages/html/stories/SwimLanes.stories.js
+++ b/packages/html/stories/SwimLanes.stories.ts
@@ -27,6 +27,11 @@ import {
   StackLayout,
   LayoutManager,
   Graph,
+  type Cell,
+  type EdgeParameters,
+  type EventObject,
+  type SelectionHandler,
+  type VertexParameters,
 } from '@maxgraph/core';
 import { globalTypes, globalValues } from './shared/args.js';
 import {
@@ -45,7 +50,7 @@ export default {
   },
 };
 
-const Template = ({ label, ...args }) => {
+const Template = ({ label, ...args }: Record<string, string>) => {
   configureImagesBasePath();
   const container = createGraphContainer(args);
   container.style.background = ''; // no grid
@@ -66,6 +71,10 @@ const Template = ({ label, ...args }) => {
   // const editor = new Editor(null);
   // editor.setGraphContainer(container);
   // const { graph } = editor;
+  type CustomGraph = Graph & {
+    isPool(cell: Cell | null): boolean;
+  };
+
   const graph = new Graph(container);
   const model = graph.getDataModel();
 
@@ -75,7 +84,7 @@ const Template = ({ label, ...args }) => {
   graph.setResizeContainer(true);
   configureExpandedAndCollapsedImages(graph);
 
-  const graphHandler = graph.getPlugin('SelectionHandler');
+  const graphHandler = graph.getPlugin('SelectionHandler') as SelectionHandler;
   graphHandler.setRemoveCellsFromParent(false);
 
   // Changes the default vertex style in-place
@@ -150,11 +159,10 @@ const Template = ({ label, ...args }) => {
 
     // End-states are no valid sources
     const previousIsValidSource = graph.isValidSource;
-
     graph.isValidSource = function (cell) {
-      if (previousIsValidSource.apply(this, arguments)) {
+      if (previousIsValidSource.apply(this, [cell]) && cell) {
         const style = cell.getStyle();
-        return style == null || !style.baseStyleNames.includes('end');
+        return style == null || !style.baseStyleNames?.includes('end');
       }
       return false;
     };
@@ -166,11 +174,12 @@ const Template = ({ label, ...args }) => {
     // the example below, so we use the state
     // style below
     graph.isValidTarget = function (cell) {
+      if (!cell) return false;
       const style = cell.getStyle();
       return (
         !cell.isEdge() &&
         !this.isSwimlane(cell) &&
-        (style == null || !!style.baseStyleNames.includes('state'))
+        (style == null || !!style.baseStyleNames?.includes('state'))
       );
     };
 
@@ -181,17 +190,17 @@ const Template = ({ label, ...args }) => {
     graph.setSplitEnabled(false);
 
     // Returns true for valid drop operations
-    graph.isValidDropTarget = function (target, cells, evt) {
+    graph.isValidDropTarget = function (this: CustomGraph, target, cells, evt) {
       if (this.isSplitEnabled() && this.isSplitTarget(target, cells, evt)) {
         return true;
       }
 
-      const model = this.getDataModel();
       let lane = false;
       let pool = false;
       let cell = false;
 
       // Checks if any lanes or pools are selected
+      cells ??= [];
       for (let i = 0; i < cells.length; i++) {
         const tmp = cells[i].getParent();
         lane = lane || this.isPool(tmp);
@@ -208,15 +217,15 @@ const Template = ({ label, ...args }) => {
     };
 
     // Adds new method for identifying a pool
-    graph.isPool = function (cell) {
+    (graph as CustomGraph).isPool = function (cell: Cell | null) {
       const model = this.getDataModel();
-      const parent = cell.getParent();
+      const parent = cell?.getParent();
 
-      return parent != null && parent.getParent() == model.getRoot();
+      return parent?.getParent() == model.getRoot();
     };
 
     // Keeps widths on collapse/expand
-    const foldingHandler = function (sender, evt) {
+    const foldingHandler = function (_sender: any, evt: EventObject) {
       const cells = evt.getProperty('cells');
 
       for (let i = 0; i < cells.length; i++) {
@@ -232,17 +241,16 @@ const Template = ({ label, ...args }) => {
   }
 
   // Changes swimlane orientation while collapsed
-  const getStyle = function () {
-    // TODO super cannot be used here
-    // let style = super.getStyle();
-    let style = {};
-
-    if (this.isCollapsed()) {
-      style.horizontal = true;
-      style.align = 'left';
-      style.spacingLeft = 14;
+  const getStyle = function (this: Cell) {
+    if (!this.isCollapsed()) {
+      return this.style;
     }
-
+    // Need to create a copy the original style as we don't want to change the original style stored in the Cell
+    // Otherwise, when expanding the cell, the style will be incorrect
+    const style = { ...this.style };
+    style.horizontal = true;
+    style.align = 'left';
+    style.spacingLeft = 14;
     return style;
   };
 
@@ -268,11 +276,12 @@ const Template = ({ label, ...args }) => {
 
   layoutMgr.getLayout = function (cell) {
     if (
+      cell &&
       !cell.isEdge() &&
       cell.getChildCount() > 0 &&
-      (cell.getParent() == model.getRoot() || graph.isPool(cell))
+      (cell.getParent() == model.getRoot() || (graph as CustomGraph).isPool(cell))
     ) {
-      layout.fill = graph.isPool(cell);
+      layout.fill = (graph as CustomGraph).isPool(cell);
 
       return layout;
     }
@@ -284,17 +293,15 @@ const Template = ({ label, ...args }) => {
   // is normally the first child of the root (ie. layer 0).
   const parent = graph.getDefaultParent();
 
-  const insertVertex = (options) => {
+  const insertVertex = (options: VertexParameters) => {
     const v = graph.insertVertex(options);
-    // TODO find a way to restore
-    // v.getStyle = getStyle;
+    v.getStyle = getStyle;
     return v;
   };
 
-  const insertEdge = (options) => {
+  const insertEdge = (options: EdgeParameters) => {
     const e = graph.insertEdge(options);
-    // TODO find a way to restore
-    // e.getStyle = getStyle;
+    e.getStyle = getStyle;
     return e;
   };
 
@@ -458,8 +465,6 @@ const Template = ({ label, ...args }) => {
       style: { baseStyleNames: ['end'] },
     });
 
-    let e = null;
-
     insertEdge({
       parent: lane1a,
       source: start1,
@@ -514,7 +519,11 @@ const Template = ({ label, ...args }) => {
       value: 'Yes',
       source: step44,
       target: step111,
-      style: { verticalAlign: 'bottom', horizontal: 0, labelBackgroundColor: 'white' },
+      style: {
+        verticalAlign: 'bottom',
+        horizontal: false,
+        labelBackgroundColor: 'white',
+      },
     });
 
     insertEdge({
@@ -524,7 +533,7 @@ const Template = ({ label, ...args }) => {
       target: end2,
       style: { verticalAlign: 'bottom' },
     });
-    e = insertEdge({
+    let e = insertEdge({
       parent: lane2a,
       value: 'No',
       source: step444,
@@ -532,10 +541,10 @@ const Template = ({ label, ...args }) => {
       style: { verticalAlign: 'top' },
     });
 
-    e.geometry.points = [
+    e.geometry!.points = [
       new Point(
-        step444.geometry.x + step444.geometry.width / 2,
-        end3.geometry.y + end3.geometry.height / 2
+        step444.geometry!.x + step444.geometry!.width / 2,
+        end3.geometry!.y + end3.geometry!.height / 2
       ),
     ];
 
@@ -558,10 +567,10 @@ const Template = ({ label, ...args }) => {
       style: { baseStyleNames: ['crossover'] },
     });
 
-    e.geometry.points = [
+    e.geometry!.points = [
       new Point(
-        step33.geometry.x + step33.geometry.width / 2 + 20,
-        step11.geometry.y + (step11.geometry.height * 4) / 5
+        step33.geometry!.x + step33.geometry!.width / 2 + 20,
+        step11.geometry!.y + (step11.geometry!.height * 4) / 5
       ),
     ];
 
diff --git a/packages/html/stories/Toolbar.stories.ts b/packages/html/stories/Toolbar.stories.ts
index b6596d91f..37bdefc2b 100644
--- a/packages/html/stories/Toolbar.stories.ts
+++ b/packages/html/stories/Toolbar.stories.ts
@@ -35,7 +35,11 @@ import {
   rubberBandTypes,
   rubberBandValues,
 } from './shared/args.js';
-import { createGraphContainer } from './shared/configure.js';
+import {
+  configureExpandedAndCollapsedImages,
+  configureImagesBasePath,
+  createGraphContainer,
+} from './shared/configure.js';
 // style required by RubberBand
 import '@maxgraph/core/css/common.css';
 
@@ -52,6 +56,7 @@ export default {
 };
 
 const Template = ({ label, ...args }: { [p: string]: any }) => {
+  configureImagesBasePath();
   const div = document.createElement('div');
   const container = createGraphContainer(args);
   div.appendChild(container);
@@ -80,6 +85,7 @@ const Template = ({ label, ...args }: { [p: string]: any }) => {
   // using the fastest rendering available on the browser
   const model = new GraphDataModel();
   const graph = new Graph(container, model);
+  configureExpandedAndCollapsedImages(graph);
   graph.dropEnabled = true;
 
   // Matches DnD inside the graph


Test patch:
diff --git a/packages/core/__tests__/view/style/StyleSheet.test.ts b/packages/core/__tests__/view/style/StyleSheet.test.ts
index e1604219d..cca66bf51 100644
--- a/packages/core/__tests__/view/style/StyleSheet.test.ts
+++ b/packages/core/__tests__/view/style/StyleSheet.test.ts
@@ -199,25 +199,27 @@ describe('getCellStyle', () => {
   });
 
   // This was the mxGraph behaviour, see https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/src/js/view/mxStylesheet.js#L236-L239
-  test('Setting properties with "none" value fallback to property value of the default and "base" styles', () => {
+  test('Setting properties with "none" value remove the property value', () => {
     const stylesheet = new Stylesheet();
     stylesheet.putCellStyle('style-1', {
       fillColor: 'orange',
+      rounded: true,
     });
 
     const cellStyle = stylesheet.getCellStyle(
       {
         baseStyleNames: ['style-1'],
         fillColor: NONE,
+        gradientColor: NONE, // not set in default and baseStyles
         shape: 'cloud',
         strokeColor: NONE,
       },
-      { strokeColor: 'pink' }
+      { opacity: 50, strokeColor: 'pink' }
     );
     expect(cellStyle).toStrictEqual({
-      fillColor: 'orange',
+      opacity: 50, // from default
+      rounded: true, // from style-1
       shape: 'cloud',
-      strokeColor: 'pink',
     });
   });