Repository: maxGraph/maxGraph
Pull request #410 (2024-05-22 14:43:32)

refactor: simplify CellRenderer.checkPlaceholderStyles
Add tests and improve some JSDoc.


Patch:
diff --git a/.github/ISSUE_TEMPLATE/03-technical-request.md b/.github/ISSUE_TEMPLATE/03-technical-request.md
index b19522984..2757927cf 100644
--- a/.github/ISSUE_TEMPLATE/03-technical-request.md
+++ b/.github/ISSUE_TEMPLATE/03-technical-request.md
@@ -1,6 +1,6 @@
 ---
 name: Technical request
-about: Suggest an refactoring, technical change, ... for this project
+about: Suggest a refactoring, technical change, ... for this project
 title: "[CHORE]"
 labels: chore, triage
 assignees: ''
diff --git a/.github/ISSUE_TEMPLATE/04-documentation-request.md b/.github/ISSUE_TEMPLATE/04-documentation-request.md
new file mode 100644
index 000000000..8fc508e67
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/04-documentation-request.md
@@ -0,0 +1,20 @@
+---
+name: Documentation request
+about: Suggest a refactoring, technical change, ... for this project
+title: "[DOC]"
+labels: documentation, triage
+assignees: ''
+
+---
+
+**Please first consider** starting discussion in the [Q&A discussions](https://github.com/maxGraph/maxGraph/discussions/categories/q-a) prior submitting documentation requests.
+
+### Is your documentation request related to a problem? Please describe.
+A clear and concise description of what the problem is. Ex. I'm always frustrated about [...]
+
+### Describe the solution you'd like
+A clear and concise description of what you want clarified in the documentation.
+
+### Additional context
+Add any other context or screenshots about the request here.
+
diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml
index de6a8d408..70f1ba423 100644
--- a/.github/workflows/create-github-release.yml
+++ b/.github/workflows/create-github-release.yml
@@ -25,7 +25,7 @@ jobs:
 
             npm package: [${{ env.VERSION }}](https://www.npmjs.com/package/@maxgraph/core/v/${{ env.VERSION }})
             **Update the milestone URL**
-            Issues: [milestone ${{ env.VERSION }}](https://github.com/maxGraph/maxGraph/milestone/x?closed=1)
+            Fixed issues: [milestone ${{ env.VERSION }}](https://github.com/maxGraph/maxGraph/milestone/x?closed=1)
             See also the [Changelog](https://github.com/maxGraph/maxGraph/tree/v${{ env.VERSION }}/CHANGELOG.md) file.
 
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 793de476e..c100c0600 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # `maxGraph` Change Log
 
+## 0.10.2
+
+Release date: `2024-05-24`
+
+For more details, see the [0.10.2 Changelog](https://github.com/maxGraph/maxGraph/releases/tag/v0.10.2) on the GitHub release page.
+
+This release contains bug fixes.
+
 ## 0.10.1
 
 Release date: `2024-04-23`
diff --git a/package-lock.json b/package-lock.json
index 0e1bcfd87..2c2917a69 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11490,9 +11490,9 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "node_modules/ejs": {
-      "version": "3.1.9",
-      "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
-      "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+      "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
       "dev": true,
       "dependencies": {
         "jake": "^10.8.5"
@@ -28219,7 +28219,7 @@
     },
     "packages/core": {
       "name": "@maxgraph/core",
-      "version": "0.10.1",
+      "version": "0.10.2",
       "license": "Apache-2.0",
       "devDependencies": {
         "jest": "^29.3.1",
diff --git a/packages/core/css/common.css b/packages/core/css/common.css
index f7eb430ae..21f389211 100644
--- a/packages/core/css/common.css
+++ b/packages/core/css/common.css
@@ -25,7 +25,7 @@ div.mxRubberband {
 	padding: 0px;
 	margin: 0px;
 }
-div.MaxWindow {
+div.mxWindow {
 	-webkit-box-shadow: 3px 3px 12px #C0C0C0;
 	-moz-box-shadow: 3px 3px 12px #C0C0C0;
 	box-shadow: 3px 3px 12px #C0C0C0;
@@ -36,7 +36,7 @@ div.MaxWindow {
 	overflow: hidden;
 	z-index: 1;
 }
-table.MaxWindow {
+table.mxWindow {
 	border-collapse: collapse;
 	table-layout: fixed;
   	font-family: Arial;
diff --git a/packages/core/package.json b/packages/core/package.json
index 6028b9de4..01416189f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -2,7 +2,7 @@
   "name": "@maxgraph/core",
   "license": "Apache-2.0",
   "private": false,
-  "version": "0.10.1",
+  "version": "0.10.2",
   "type": "module",
   "description": "maxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.",
   "keywords": [
diff --git a/packages/core/src/Client.ts b/packages/core/src/Client.ts
index fcd30a36a..846fba40a 100644
--- a/packages/core/src/Client.ts
+++ b/packages/core/src/Client.ts
@@ -20,7 +20,7 @@ class Client {
   /**
    * The version of the `maxGraph` library.
    */
-  static VERSION = '0.10.1';
+  static VERSION = '0.10.2';
 
   /**
    * Base path for all URLs in the core without trailing slash.
diff --git a/packages/core/src/gui/MaxWindow.ts b/packages/core/src/gui/MaxWindow.ts
index 2cb6af317..54cec9c83 100644
--- a/packages/core/src/gui/MaxWindow.ts
+++ b/packages/core/src/gui/MaxWindow.ts
@@ -188,7 +188,7 @@ class MaxWindow extends EventSource {
     minimizable = true,
     movable = true,
     replaceNode: HTMLElement | null = null,
-    style = ''
+    style?: string
   ) {
     super();
 
@@ -235,7 +235,7 @@ class MaxWindow extends EventSource {
     y: number,
     width: number | null = null,
     height: number | null = null,
-    style = 'MaxWindow'
+    style = 'mxWindow'
   ): void {
     this.div = document.createElement('div');
     this.div.className = style;
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/cell/CellRenderer.ts b/packages/core/src/view/cell/CellRenderer.ts
index dd45d94fa..c7dc72bb2 100644
--- a/packages/core/src/view/cell/CellRenderer.ts
+++ b/packages/core/src/view/cell/CellRenderer.ts
@@ -50,15 +50,23 @@ import { CellStateStyle } from '../../types';
 import SelectionCellsHandler from '../handler/SelectionCellsHandler';
 import { Graph } from '../Graph';
 
+const placeholderStyleValues = ['inherit', 'swimlane', 'indicated'];
+const placeholderStyleProperties: (keyof CellStateStyle)[] = [
+  'fillColor',
+  'strokeColor',
+  'gradientColor',
+  'fontColor',
+];
+
 /**
  * Renders cells into a document object model. The <defaultShapes> is a global
- * map of shapename, constructor pairs that is used in all instances. You can
+ * map of shape names, constructor pairs that is used in all instances. You can
  * get a list of all available shape names using the following code.
  *
  * In general the cell renderer is in charge of creating, redrawing and
  * destroying the shape and label associated with a cell state, as well as
  * some other graphical objects, namely controls and overlays. The shape
- * hieararchy in the display (ie. the hierarchy in which the DOM nodes
+ * hierarchy in the display (i.e. the hierarchy in which the DOM nodes
  * appear in the document) does not reflect the cell hierarchy. The shapes
  * are a (flat) sequence of shapes and labels inside the draw pane of the
  * graph view, with some exceptions, namely the HTML labels being placed
@@ -71,51 +79,54 @@ import { Graph } from '../Graph';
  *   MaxLog.debug(i);
  * }
  * ```
- *
- * Constructor: mxCellRenderer
- *
- * Constructs a new cell renderer with the following built-in shapes:
- * arrow, rectangle, ellipse, rhombus, image, line, label, cylinder,
- * swimlane, connector, actor and cloud.
  */
 class CellRenderer {
   /**
    * Static array that contains the globally registered shapes which are
    * known to all instances of this class. For adding new shapes you should
    * use the static {@link CellRenderer#registerShape} function.
+   *
+   * Built-in shapes: arrow, rectangle, ellipse, rhombus, image, line, label, cylinder,
+   * swimlane, connector, actor and cloud.
    */
   static defaultShapes: { [key: string]: typeof Shape } = {};
 
   /**
-   * Defines the default shape for edges. Default is {@link Connector}.
+   * Defines the default shape for edges.
+   * @default {@link ConnectorShape}
    */
   // @ts-expect-error The constructors for Shape and Connector are different.
   defaultEdgeShape: typeof Shape = ConnectorShape;
 
   /**
-   * Defines the default shape for vertices. Default is {@link RectangleShape}.
+   * Defines the default shape for vertices.
+   * @default {@link RectangleShape}.
    */
   defaultVertexShape: typeof RectangleShape = RectangleShape;
 
   /**
-   * Defines the default shape for labels. Default is {@link Text}.
+   * Defines the default shape for labels.
+   * @default {@link Text}.
    */
   defaultTextShape: typeof TextShape = TextShape;
 
   /**
    * Specifies if the folding icon should ignore the horizontal
-   * orientation of a swimlane. Default is true.
+   * orientation of a swimlane.
+   * @default true.
    */
   legacyControlPosition = true;
 
   /**
    * Specifies if spacing and label position should be ignored if overflow is
-   * fill or width. Default is true for backwards compatiblity.
+   * fill or width.
+   * @default true for backwards compatibility.
    */
   legacySpacing = true;
 
   /**
-   * Anti-aliasing option for new shapes. Default is true.
+   * Antialiasing option for new shapes.
+   * @default true.
    */
   antiAlias = true;
 
@@ -126,7 +137,8 @@ class CellRenderer {
 
   /**
    * Specifies if the enabled state of the graph should be ignored in the control
-   * click handler (to allow folding in disabled graphs). Default is false.
+   * click handler (to allow folding in disabled graphs).
+   * @default false.
    */
   forceControlClickHandler = false;
 
@@ -134,7 +146,7 @@ class CellRenderer {
    * Registers the given constructor under the specified key in this instance of the renderer.
    * @example
    * ```
-   * mxCellRenderer.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape);
+   * CellRenderer.registerShape(Constants.SHAPE_RECTANGLE, RectangleShape);
    * ```
    *
    * @param key the shape name.
@@ -146,7 +158,7 @@ class CellRenderer {
 
   /**
    * Initializes the shape in the given state by calling its init method with
-   * the correct container after configuring it using <configureShape>.
+   * the correct container after configuring it using {@link configureShape}.
    *
    * @param state <CellState> for which the shape should be initialized.
    */
@@ -161,7 +173,7 @@ class CellRenderer {
   /**
    * Creates and returns the shape for the given cell state.
    *
-   * @param state <CellState> for which the shape should be created.
+   * @param state {@link CellState} for which the shape should be created.
    */
   createShape(state: CellState) {
     let shape = null;
@@ -183,7 +195,7 @@ class CellRenderer {
   /**
    * Creates the indicator shape for the given cell state.
    *
-   * @param state <CellState> for which the indicator shape should be created.
+   * @param state {@link CellState} for which the indicator shape should be created.
    */
   createIndicatorShape(state: CellState) {
     if (state.shape) {
@@ -192,7 +204,7 @@ class CellRenderer {
   }
 
   /**
-   * Returns the shape for the given name from <defaultShapes>.
+   * Returns the shape for the given name from {@link defaultShapes}.
    */
   getShape(name: string | null) {
     return name ? CellRenderer.defaultShapes[name] : null;
@@ -251,24 +263,13 @@ class CellRenderer {
   }
 
   /**
-   * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets
-   * the respective color on the shape.
+   * Check if style properties supporting placeholders requires resolution.
    */
   checkPlaceholderStyles(state: CellState) {
     // LATER: Check if the color has actually changed
-    if (state.style) {
-      const values = ['inherit', 'swimlane', 'indicated'];
-      const styles: (keyof CellStateStyle)[] = [
-        'fillColor',
-        'strokeColor',
-        'gradientColor',
-        'fontColor',
-      ];
-
-      for (let i = 0; i < styles.length; i += 1) {
-        if (values.indexOf(state.style[styles[i]] as string) >= 0) {
-          return true;
-        }
+    for (const property of placeholderStyleProperties) {
+      if (placeholderStyleValues.includes(state.style[property] as string)) {
+        return true;
       }
     }
     return false;
@@ -282,7 +283,7 @@ class CellRenderer {
     const shape: Shape | null = key === 'fontColor' ? state.text : state.shape;
 
     if (shape) {
-      const graph = <Graph>state.view.graph;
+      const graph = state.view.graph;
 
       // @ts-ignore
       const value = shape[field];
@@ -301,7 +302,7 @@ class CellRenderer {
           referenced = state.cell;
         }
 
-        referenced = graph.getSwimlane(<Cell>referenced);
+        referenced = graph.getSwimlane(referenced);
         key = graph.swimlaneIndicatorColorAttribute;
       } else if (value === 'indicated' && state.shape) {
         // @ts-ignore
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/HelloPort.stories.ts b/packages/html/stories/HelloPort.stories.ts
index 788e3b1b7..95aab27c0 100644
--- a/packages/html/stories/HelloPort.stories.ts
+++ b/packages/html/stories/HelloPort.stories.ts
@@ -32,8 +32,7 @@ import {
   rubberBandValues,
 } from './shared/args.js';
 import { configureImagesBasePath, createGraphContainer } from './shared/configure.js';
-// style required by RubberBand and popup
-import '@maxgraph/core/css/common.css';
+import '@maxgraph/core/css/common.css'; // style required by RubberBand and popup
 
 export default {
   title: 'Connections/HelloPort',
diff --git a/packages/html/stories/JsonData.stories.ts b/packages/html/stories/JsonData.stories.ts
index 0c005f494..6aa2d0d93 100644
--- a/packages/html/stories/JsonData.stories.ts
+++ b/packages/html/stories/JsonData.stories.ts
@@ -34,8 +34,7 @@ import {
   rubberBandValues,
 } from './shared/args.js';
 import { configureImagesBasePath, createGraphContainer } from './shared/configure.js';
-// style required by RubberBand
-import '@maxgraph/core/css/common.css';
+import '@maxgraph/core/css/common.css'; // style required by RubberBand and popup
 
 export default {
   title: 'Xml_Json/JsonData',
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
diff --git a/packages/html/stories/UserObject.stories.ts b/packages/html/stories/UserObject.stories.ts
index 10105d668..6b0e9c750 100644
--- a/packages/html/stories/UserObject.stories.ts
+++ b/packages/html/stories/UserObject.stories.ts
@@ -39,6 +39,7 @@ import {
   rubberBandValues,
 } from './shared/args.js';
 import { configureImagesBasePath, createGraphContainer } from './shared/configure.js';
+import '@maxgraph/core/css/common.css'; // style required by popup
 
 export default {
   title: 'Xml_Json/UserObject',
diff --git a/packages/html/stories/Window.stories.js b/packages/html/stories/Window.stories.ts
similarity index 91%
rename from packages/html/stories/Window.stories.js
rename to packages/html/stories/Window.stories.ts
index 7dba159c7..822277d86 100644
--- a/packages/html/stories/Window.stories.js
+++ b/packages/html/stories/Window.stories.ts
@@ -33,8 +33,7 @@ import {
   rubberBandValues,
 } from './shared/args.js';
 import { configureImagesBasePath, createGraphContainer } from './shared/configure.js';
-// style required by RubberBand
-import '@maxgraph/core/css/common.css';
+import '@maxgraph/core/css/common.css'; // style required by RubberBand and MaxWindow/MaxLog
 
 export default {
   title: 'Windows/Windows',
@@ -50,7 +49,7 @@ export default {
   },
 };
 
-const Template = ({ label, ...args }) => {
+const Template = ({ label, ...args }: Record<string, string>) => {
   configureImagesBasePath();
   const container = createGraphContainer(args);
 
@@ -88,12 +87,13 @@ const Template = ({ label, ...args }) => {
   graph.batchUpdate(() => {
     const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
     const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
-    const e1 = graph.insertEdge(parent, null, '', v1, v2);
+    graph.insertEdge(parent, null, '', v1, v2);
   });
 
   wnd.setMaximizable(true);
   wnd.setResizable(true);
   wnd.setVisible(true);
+  wnd.setClosable(true);
 
   const lorem =
     'Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ';
@@ -114,8 +114,9 @@ const Template = ({ label, ...args }) => {
   wnd.setScrollable(true);
   wnd.setResizable(true);
   wnd.setVisible(true);
+  wnd.setClosable(true);
 
-  content = content.cloneNode(true);
+  content = content.cloneNode(true) as HTMLDivElement;
   content.style.width = '400px';
 
   wnd = new MaxWindow(
@@ -132,6 +133,7 @@ const Template = ({ label, ...args }) => {
   wnd.setScrollable(true);
   wnd.setResizable(true);
   wnd.setVisible(true);
+  wnd.setClosable(true);
 
   MaxLog.show();
 


Test patch:
diff --git a/packages/core/__tests__/GraphDataModel.test.ts b/packages/core/__tests__/GraphDataModel.test.ts
index 5527f4b48..e0110044a 100644
--- a/packages/core/__tests__/GraphDataModel.test.ts
+++ b/packages/core/__tests__/GraphDataModel.test.ts
@@ -34,6 +34,6 @@ describe('isLayer', () => {
     const child = new Cell();
     root.children.push(child);
     child.setParent(root);
-    expect(dm.isLayer(child)).toBe(true);
+    expect(dm.isLayer(child)).toBeTruthy();
   });
 });
diff --git a/packages/core/__tests__/view/cell/CellRenderer.test.ts b/packages/core/__tests__/view/cell/CellRenderer.test.ts
new file mode 100644
index 000000000..3ce9b831c
--- /dev/null
+++ b/packages/core/__tests__/view/cell/CellRenderer.test.ts
@@ -0,0 +1,43 @@
+/*
+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 { describe, expect, test } from '@jest/globals';
+import { CellRenderer, CellState, CellStateStyle } from '../../../src';
+
+describe('checkPlaceholderStyles', () => {
+  test('CellState with unset style', () => {
+    expect(new CellRenderer().checkPlaceholderStyles(new CellState())).toBeFalsy();
+  });
+
+  test('no matching element in style', () => {
+    const cellState = new CellState();
+    cellState.style = { shape: 'rectangle' };
+    expect(new CellRenderer().checkPlaceholderStyles(cellState)).toBeFalsy();
+  });
+
+  describe.each(['fillColor', 'strokeColor'] as Array<keyof CellStateStyle>)(
+    'with one matching element in style - property=%s',
+    (property: string) => {
+      test.each(['inherit', 'swimlane'])('placeholder=%s', (placeholder: string) => {
+        const cellState = new CellState();
+        cellState.style = { arcSize: 10, rounded: true };
+        // @ts-ignore
+        cellState.style[property] = placeholder;
+        expect(new CellRenderer().checkPlaceholderStyles(cellState)).toBeTruthy();
+      });
+    }
+  );
+});
diff --git a/packages/core/__tests__/view/layout/HierarchicalLayout.test.ts b/packages/core/__tests__/view/layout/HierarchicalLayout.test.ts
index 5f053ee67..bf5d6dd43 100644
--- a/packages/core/__tests__/view/layout/HierarchicalLayout.test.ts
+++ b/packages/core/__tests__/view/layout/HierarchicalLayout.test.ts
@@ -15,17 +15,13 @@ limitations under the License.
 */
 
 import { describe, expect, test } from '@jest/globals';
-import { Graph, HierarchicalLayout } from '../../../src';
-
-function createGraph(): Graph {
-  // @ts-ignore - no need for a container, we don't check the view here
-  return new Graph(null);
-}
+import { HierarchicalLayout } from '../../../src';
+import { createGraphWithoutContainer } from '../../utils';
 
 describe('layout execute', () => {
   test('Circular layout graph is created successfully', () => {
     // prepare test
-    const graph: Graph = createGraph();
+    const graph = createGraphWithoutContainer();
     const parent = graph.getDefaultParent();
     const layout = new HierarchicalLayout(graph);
 
@@ -72,7 +68,7 @@ describe('layout execute', () => {
 
   test('Non-circular layout graph is created successfully', () => {
     // prepare test
-    const graph: Graph = createGraph();
+    const graph = createGraphWithoutContainer();
     const parent = graph.getDefaultParent();
     const layout = new HierarchicalLayout(graph);
 
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',
     });
   });