From 2f0c69b7e8acac1852a6e71f843ee113d8a517b0 Mon Sep 17 00:00:00 2001
From: ghiscoding <gbeaulac@gmail.com>
Date: Sat, 18 Jan 2025 17:35:20 -0500
Subject: [PATCH 1/3] feat(vue): add rowspan to Slickgrid-Vue - also
 move/rename Example 43 as Example 17 since the Remote Example is long gone

---
 demos/vue/src/components/Example17.vue        | 157 +++++
 demos/vue/src/components/Example42.vue        |   2 +-
 demos/vue/src/components/Example43.vue        | 570 +++++++++++++++---
 demos/vue/src/components/Example44.vue        | 348 +++++++++++
 demos/vue/src/router/index.ts                 |   6 +-
 demos/vue/test/cypress/e2e/example17.cy.ts    |  83 +++
 demos/vue/test/cypress/e2e/example43.cy.ts    | 439 ++++++++++++--
 demos/vue/test/cypress/e2e/example44.cy.ts    | 458 ++++++++++++++
 .../column-row-spanning.md                    |   6 +
 .../src/examples/example33.ts                 |   8 -
 .../column-row-spanning.md                    |   6 +
 .../src/components/SlickgridVue.vue           |   9 +-
 .../src/interfaces/itemMetadata.interface.ts  |   4 +-
 .../e2e/{example32.cy.ts => example43.cy.ts}  |   0
 .../e2e/{example33.cy.ts => example44.cy.ts}  |   0
 15 files changed, 1925 insertions(+), 171 deletions(-)
 create mode 100644 demos/vue/src/components/Example17.vue
 create mode 100644 demos/vue/src/components/Example44.vue
 create mode 100644 demos/vue/test/cypress/e2e/example17.cy.ts
 create mode 100644 demos/vue/test/cypress/e2e/example44.cy.ts
 rename test/cypress/e2e/{example32.cy.ts => example43.cy.ts} (100%)
 rename test/cypress/e2e/{example33.cy.ts => example44.cy.ts} (100%)

diff --git a/demos/vue/src/components/Example17.vue b/demos/vue/src/components/Example17.vue
new file mode 100644
index 000000000..8007d4958
--- /dev/null
+++ b/demos/vue/src/components/Example17.vue
@@ -0,0 +1,157 @@
+<script setup lang="ts">
+import { ExcelExportService } from '@slickgrid-universal/excel-export';
+import { type Column, type GridOption, SlickgridVue, toCamelCase } from 'slickgrid-vue';
+import { ref } from 'vue';
+
+const gridCreated = ref(false);
+const gridOptions = ref<GridOption>();
+const columnDefinitions = ref<Column[]>([]);
+const dataset = ref<any[]>([]);
+const templateUrl = ref(new URL('./data/users.csv', import.meta.url).href);
+const uploadFileRef = ref('');
+const showSubTitle = ref(true);
+
+function disposeGrid() {
+  gridCreated.value = false;
+}
+
+function handleFileImport(event: any) {
+  const file: File = event.target.files[0];
+  if (file.name.endsWith('.csv')) {
+    const reader = new FileReader();
+    reader.onload = (e: any) => {
+      const content = e.target.result;
+      dynamicallyCreateGrid(content);
+    };
+    reader.readAsText(file);
+  } else {
+    alert('File must be a CSV file');
+  }
+}
+
+function handleDefaultCsv() {
+  const staticDataCsv = `First Name,Last Name,Age,Type\nBob,Smith,33,Teacher\nJohn,Doe,20,Student\nJane,Doe,21,Student`;
+  dynamicallyCreateGrid(staticDataCsv);
+  uploadFileRef.value = '';
+}
+
+function dynamicallyCreateGrid(csvContent: string) {
+  // dispose of any previous grid before creating a new one
+  gridCreated.value = false;
+
+  const dataRows = csvContent?.split('\n');
+  const colDefs: Column[] = [];
+  const outputData: any[] = [];
+
+  // create column definitions
+  dataRows.forEach((dataRow, rowIndex) => {
+    const cellValues = dataRow.split(',');
+    const dataEntryObj: any = {};
+
+    if (rowIndex === 0) {
+      // the 1st row is considered to be the header titles, we can create the column definitions from it
+      for (const cellVal of cellValues) {
+        const camelFieldName = toCamelCase(cellVal);
+        colDefs.push({
+          id: camelFieldName,
+          name: cellVal,
+          field: camelFieldName,
+          filterable: true,
+          sortable: true,
+        });
+      }
+    } else {
+      // at this point all column defs were created and we can loop through them and
+      // we can now start adding data as an object and then simply push it to the dataset array
+      cellValues.forEach((cellVal, colIndex) => {
+        dataEntryObj[colDefs[colIndex].id] = cellVal;
+      });
+
+      // a unique "id" must be provided, if not found then use the row index and push it to the dataset
+      if ('id' in dataEntryObj) {
+        outputData.push(dataEntryObj);
+      } else {
+        outputData.push({ ...dataEntryObj, id: rowIndex });
+      }
+    }
+  });
+
+  gridOptions.value = {
+    gridHeight: 300,
+    gridWidth: 800,
+    enableFiltering: true,
+    enableExcelExport: true,
+    externalResources: [new ExcelExportService()],
+    headerRowHeight: 35,
+    rowHeight: 33,
+  };
+
+  dataset.value = outputData;
+  columnDefinitions.value = colDefs;
+  gridCreated.value = true;
+}
+
+function toggleSubTitle() {
+  showSubTitle.value = !showSubTitle.value;
+  const action = showSubTitle.value ? 'remove' : 'add';
+  document.querySelector('.subtitle')?.classList[action]('hidden');
+}
+</script>
+
+<template>
+  <h2>
+    Example 17: Dynamically Create Grid from CSV / Excel import
+    <span class="float-end">
+      <a
+        style="font-size: 18px"
+        target="_blank"
+        href="https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vue/src/components/Example17.vue"
+      >
+        <span class="mdi mdi-link-variant"></span> code
+      </a>
+    </span>
+    <button class="ms-2 btn btn-outline-secondary btn-sm btn-icon" type="button" data-test="toggle-subtitle" @click="toggleSubTitle()">
+      <span class="mdi mdi-information-outline" title="Toggle example sub-title details"></span>
+    </button>
+  </h2>
+
+  <div class="subtitle">
+    Allow creating a grid dynamically by importing an external CSV or Excel file. This script demo will read the CSV file and will consider
+    the first row as the column header and create the column definitions accordingly, while the next few rows will be considered the
+    dataset. Note that this example is demoing a CSV file import but in your application you could easily implemnt an Excel file uploading.
+  </div>
+
+  <div>A default CSV file can be download <a id="template-dl" :href="templateUrl">here</a>.</div>
+
+  <div class="d-flex mt-5 align-items-end">
+    <div class="file-upload">
+      <label for="formFile" class="form-label">Choose a CSV file…</label>
+      <input class="form-control" type="file" data-test="file-upload-input" :value="uploadFileRef" @change="handleFileImport" />
+    </div>
+    <span class="mx-3">or</span>
+    <div>
+      <button id="uploadBtn" data-test="static-data-btn" class="btn btn-outline-secondary" @click="handleDefaultCsv">
+        Use default CSV data
+      </button>
+      &nbsp;/
+      <button class="btn btn-outline-danger btn-sm ms-2" @click="disposeGrid()">Destroy Grid</button>
+    </div>
+  </div>
+
+  <hr />
+
+  <slickgrid-vue
+    v-if="gridCreated"
+    v-model:options="gridOptions"
+    v-model:columns="columnDefinitions as Column[]"
+    v-model:data="dataset"
+    grid-id="grid17"
+  >
+  </slickgrid-vue>
+</template>
+
+<style lang="scss">
+.file-upload {
+  max-width: 300px;
+}
+</style>
diff --git a/demos/vue/src/components/Example42.vue b/demos/vue/src/components/Example42.vue
index 911194198..14480b9f9 100644
--- a/demos/vue/src/components/Example42.vue
+++ b/demos/vue/src/components/Example42.vue
@@ -142,7 +142,7 @@ function defineGrid() {
     },
     enableExcelCopyBuffer: true,
     enableFiltering: true,
-    customPaginationComponent: CustomPagerComponent as DefineComponent<any, BasePaginationModel>, // load our Custom Pagination Component
+    customPaginationComponent: CustomPagerComponent as DefineComponent<any, BasePaginationModel, any>, // load our Custom Pagination Component
     enablePagination: true,
     pagination: {
       pageSize: pageSize.value,
diff --git a/demos/vue/src/components/Example43.vue b/demos/vue/src/components/Example43.vue
index d5c46a16c..4b8d1115d 100644
--- a/demos/vue/src/components/Example43.vue
+++ b/demos/vue/src/components/Example43.vue
@@ -1,94 +1,400 @@
 <script setup lang="ts">
 import { ExcelExportService } from '@slickgrid-universal/excel-export';
-import { type Column, type GridOption, SlickgridVue, toCamelCase } from 'slickgrid-vue';
-import { ref } from 'vue';
+import { type Column, Editors, type GridOption, type ItemMetadata, SlickgridVue, type SlickgridVueInstance } from 'slickgrid-vue';
+import { onBeforeMount, ref } from 'vue';
 
-const gridCreated = ref(false);
+const isEditable = ref(false);
 const gridOptions = ref<GridOption>();
 const columnDefinitions = ref<Column[]>([]);
 const dataset = ref<any[]>([]);
-const templateUrl = ref(new URL('./data/users.csv', import.meta.url).href);
-const uploadFileRef = ref('');
 const showSubTitle = ref(true);
+let vueGrid!: SlickgridVueInstance;
+const metadata: ItemMetadata | Record<number, ItemMetadata> = {
+  // 10001: Davolio
+  0: {
+    columns: {
+      1: { rowspan: 2 },
+      2: { colspan: 2 },
+      6: { colspan: 3 },
+      10: { colspan: 3, rowspan: 10 },
+      13: { colspan: 2 },
+      17: { colspan: 2, rowspan: 2 },
+    },
+  },
+  // 10002: (Buchanan... name not shown since Davolio has rowspan=2)
+  1: {
+    columns: {
+      3: { colspan: 3 },
+      6: { colspan: 4 },
+      13: { colspan: 2, rowspan: 5 },
+      15: { colspan: 2 },
+    },
+  },
+  // 10003: Fuller
+  2: {
+    columns: {
+      2: { colspan: 3, rowspan: 2 },
+      5: { colspan: 2 },
+      7: { colspan: 3 },
+      15: { colspan: 2 },
+      17: { colspan: 2 },
+    },
+  },
+  // 10004: Leverling
+  3: {
+    columns: {
+      6: { colspan: 4 },
+      16: { colspan: 2 },
+    },
+  },
+  // 10005: Peacock
+  4: {
+    columns: {
+      2: { colspan: 4 },
+      7: { colspan: 3 },
+      15: { colspan: 2, rowspan: 2 },
+      17: { colspan: 2 },
+    },
+  },
+  // 10006: Janet
+  5: {
+    columns: {
+      2: { colspan: 2 },
+      4: { colspan: 3 },
+      7: { colspan: 3 },
+      17: { colspan: 2 },
+    },
+  },
+  // 10007: Suyama
+  6: {
+    columns: {
+      2: { colspan: 2 },
+      5: { colspan: 2 },
+      7: { colspan: 3 },
+      14: { colspan: 2 },
+      16: { colspan: 3 },
+    },
+  },
+  // 10008: Robert
+  7: {
+    columns: {
+      2: { colspan: 3 },
+      5: { colspan: 3 },
+      13: { colspan: 3, rowspan: 2 },
+      16: { colspan: 2 },
+    },
+  },
+  // 10009: Andrew
+  8: {
+    columns: {
+      2: { colspan: 3 },
+      7: { colspan: 3, rowspan: 2 },
+      17: { colspan: 2 },
+    },
+  },
+  // 10010: Michael
+  9: {
+    columns: {
+      2: { colspan: 3 },
+      5: { colspan: 2 },
+      13: { colspan: 3 },
+      16: { colspan: 3 },
+    },
+  },
+};
 
-function disposeGrid() {
-  gridCreated.value = false;
+onBeforeMount(() => {
+  defineGrid();
+  // mock some data (different in each dataset)
+  dataset.value = loadData();
+});
+
+/* Define grid Options and Columns */
+function defineGrid() {
+  columnDefinitions.value = [
+    { id: 'employeeID', name: 'Employee ID', field: 'employeeID', minWidth: 100 },
+    { id: 'employeeName', name: 'Employee Name', field: 'employeeName', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '9:00', name: '9:00 AM', field: '9:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '9:30', name: '9:30 AM', field: '9:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '10:00', name: '10:00 AM', field: '10:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '10:30', name: '10:30 AM', field: '10:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '11:00', name: '11:00 AM', field: '11:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '11:30', name: '11:30 AM', field: '11:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '12:00', name: '12:00 PM', field: '12:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '12:30', name: '12:30 PM', field: '12:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '1:00', name: '1:00 PM', field: '1:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '1:30', name: '1:30 PM', field: '1:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '2:00', name: '2:00 PM', field: '2:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '2:30', name: '2:30 PM', field: '2:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '3:00', name: '3:00 PM', field: '3:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '3:30', name: '3:30 PM', field: '3:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '4:00', name: '4:00 PM', field: '4:00', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '4:30', name: '4:30 PM', field: '4:30', editor: { model: Editors.text }, minWidth: 120 },
+    { id: '5:00', name: '5:00 PM', field: '5:00', editor: { model: Editors.text }, minWidth: 120 },
+  ];
+
+  gridOptions.value = {
+    autoResize: {
+      bottomPadding: 30,
+      rightPadding: 50,
+    },
+    enableCellNavigation: true,
+    enableColumnReorder: true,
+    enableCellRowSpan: true,
+    enableExcelExport: true,
+    externalResources: [new ExcelExportService()],
+    enableExcelCopyBuffer: true,
+    autoEdit: true,
+    editable: false,
+    datasetIdPropertyName: 'employeeID',
+    frozenColumn: 0,
+    gridHeight: 348,
+    rowHeight: 30,
+    dataView: {
+      globalItemMetadataProvider: {
+        getRowMetadata: (_item, row) => {
+          return (metadata as Record<number, ItemMetadata>)[row];
+        },
+      },
+    },
+    rowTopOffsetRenderType: 'top', // rowspan doesn't render well with 'transform', default is 'top'
+  };
 }
 
-function handleFileImport(event: any) {
-  const file: File = event.target.files[0];
-  if (file.name.endsWith('.csv')) {
-    const reader = new FileReader();
-    reader.onload = (e: any) => {
-      const content = e.target.result;
-      dynamicallyCreateGrid(content);
-    };
-    reader.readAsText(file);
-  } else {
-    alert('File must be a CSV file');
-  }
+function navigateDown() {
+  vueGrid?.slickGrid?.navigateDown();
 }
 
-function handleDefaultCsv() {
-  const staticDataCsv = `First Name,Last Name,Age,Type\nBob,Smith,33,Teacher\nJohn,Doe,20,Student\nJane,Doe,21,Student`;
-  dynamicallyCreateGrid(staticDataCsv);
-  uploadFileRef.value = '';
+function navigateUp() {
+  vueGrid?.slickGrid?.navigateUp();
 }
 
-function dynamicallyCreateGrid(csvContent: string) {
-  // dispose of any previous grid before creating a new one
-  gridCreated.value = false;
-
-  const dataRows = csvContent?.split('\n');
-  const colDefs: Column[] = [];
-  const outputData: any[] = [];
-
-  // create column definitions
-  dataRows.forEach((dataRow, rowIndex) => {
-    const cellValues = dataRow.split(',');
-    const dataEntryObj: any = {};
-
-    if (rowIndex === 0) {
-      // the 1st row is considered to be the header titles, we can create the column definitions from it
-      for (const cellVal of cellValues) {
-        const camelFieldName = toCamelCase(cellVal);
-        colDefs.push({
-          id: camelFieldName,
-          name: cellVal,
-          field: camelFieldName,
-          filterable: true,
-          sortable: true,
-        });
-      }
-    } else {
-      // at this point all column defs were created and we can loop through them and
-      // we can now start adding data as an object and then simply push it to the dataset array
-      cellValues.forEach((cellVal, colIndex) => {
-        dataEntryObj[colDefs[colIndex].id] = cellVal;
-      });
-
-      // a unique "id" must be provided, if not found then use the row index and push it to the dataset
-      if ('id' in dataEntryObj) {
-        outputData.push(dataEntryObj);
-      } else {
-        outputData.push({ ...dataEntryObj, id: rowIndex });
-      }
-    }
-  });
+function navigateNext() {
+  vueGrid?.slickGrid?.navigateNext();
+}
 
-  gridOptions.value = {
-    gridHeight: 300,
-    gridWidth: 800,
-    enableFiltering: true,
-    enableExcelExport: true,
-    externalResources: [new ExcelExportService()],
-    headerRowHeight: 35,
-    rowHeight: 33,
-  };
+function navigatePrev() {
+  vueGrid?.slickGrid?.navigatePrev();
+}
 
-  dataset.value = outputData;
-  columnDefinitions.value = colDefs;
-  gridCreated.value = true;
+function toggleEditing() {
+  isEditable.value = !isEditable.value;
+  vueGrid.slickGrid.setOptions({ editable: isEditable.value });
+}
+
+function loadData() {
+  return [
+    {
+      employeeID: 10001,
+      employeeName: 'Davolio',
+      '9:00': 'Analysis Tasks',
+      '9:30': 'Analysis Tasks',
+      '10:00': 'Team Meeting',
+      '10:30': 'Testing',
+      '11:00': 'Development',
+      '11:30': 'Development',
+      '12:00': 'Development',
+      '12:30': 'Support',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Testing',
+      '3:00': 'Testing',
+      '3:30': 'Development',
+      '4:00': 'Conference',
+      '4:30': 'Team Meeting',
+      '5:00': 'Team Meeting',
+    },
+    {
+      employeeID: 10002,
+      employeeName: 'Buchanan',
+      '9:00': 'Task Assign',
+      '9:30': 'Support',
+      '10:00': 'Support',
+      '10:30': 'Support',
+      '11:00': 'Testing',
+      '11:30': 'Testing',
+      '12:00': 'Testing',
+      '12:30': 'Testing',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Development',
+      '3:00': 'Development',
+      '3:30': 'Check Mail',
+      '4:00': 'Check Mail',
+      '4:30': 'Team Meeting',
+      '5:00': 'Team Meeting',
+    },
+    {
+      employeeID: 10003,
+      employeeName: 'Fuller',
+      '9:00': 'Check Mail',
+      '9:30': 'Check Mail',
+      '10:00': 'Check Mail',
+      '10:30': 'Analysis Tasks',
+      '11:00': 'Analysis Tasks',
+      '11:30': 'Support',
+      '12:00': 'Support',
+      '12:30': 'Support',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Development',
+      '3:00': 'Development',
+      '3:30': 'Team Meeting',
+      '4:00': 'Team Meeting',
+      '4:30': 'Development',
+      '5:00': 'Development',
+    },
+    {
+      employeeID: 10004,
+      employeeName: 'Leverling',
+      '9:00': 'Testing',
+      '9:30': 'Check Mail',
+      '10:00': 'Check Mail',
+      '10:30': 'Support',
+      '11:00': 'Testing',
+      '11:30': 'Testing',
+      '12:00': 'Testing',
+      '12:30': 'Testing',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Development',
+      '3:00': 'Development',
+      '3:30': 'Check Mail',
+      '4:00': 'Conference',
+      '4:30': 'Conference',
+      '5:00': 'Team Meeting',
+    },
+    {
+      employeeID: 10005,
+      employeeName: 'Peacock',
+      '9:00': 'Task Assign',
+      '9:30': 'Task Assign',
+      '10:00': 'Task Assign',
+      '10:30': 'Task Assign',
+      '11:00': 'Check Mail',
+      '11:30': 'Support',
+      '12:00': 'Support',
+      '12:30': 'Support',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Development',
+      '3:00': 'Development',
+      '3:30': 'Team Meeting',
+      '4:00': 'Team Meeting',
+      '4:30': 'Testing',
+      '5:00': 'Testing',
+    },
+    {
+      employeeID: 10006,
+      employeeName: 'Janet',
+      '9:00': 'Testing',
+      '9:30': 'Testing',
+      '10:00': 'Support',
+      '10:30': 'Support',
+      '11:00': 'Support',
+      '11:30': 'Team Meeting',
+      '12:00': 'Team Meeting',
+      '12:30': 'Team Meeting',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Development',
+      '3:00': 'Development',
+      '3:30': 'Team Meeting',
+      '4:00': 'Team Meeting',
+      '4:30': 'Development',
+      '5:00': 'Development',
+    },
+    {
+      employeeID: 10007,
+      employeeName: 'Suyama',
+      '9:00': 'Analysis Tasks',
+      '9:30': 'Analysis Tasks',
+      '10:00': 'Testing',
+      '10:30': 'Development',
+      '11:00': 'Development',
+      '11:30': 'Testing',
+      '12:00': 'Testing',
+      '12:30': 'Testing',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Support',
+      '3:00': 'Build',
+      '3:30': 'Build',
+      '4:00': 'Check Mail',
+      '4:30': 'Check Mail',
+      '5:00': 'Check Mail',
+    },
+    {
+      employeeID: 10008,
+      employeeName: 'Robert',
+      '9:00': 'Task Assign',
+      '9:30': 'Task Assign',
+      '10:00': 'Task Assign',
+      '10:30': 'Development',
+      '11:00': 'Development',
+      '11:30': 'Development',
+      '12:00': 'Testing',
+      '12:30': 'Support',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Check Mail',
+      '3:00': 'Check Mail',
+      '3:30': 'Check Mail',
+      '4:00': 'Team Meeting',
+      '4:30': 'Team Meeting',
+      '5:00': 'Build',
+    },
+    {
+      employeeID: 10009,
+      employeeName: 'Andrew',
+      '9:00': 'Check Mail',
+      '9:30': 'Team Meeting',
+      '10:00': 'Team Meeting',
+      '10:30': 'Support',
+      '11:00': 'Testing',
+      '11:30': 'Development',
+      '12:00': 'Development',
+      '12:30': 'Development',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Check Mail',
+      '3:00': 'Check Mail',
+      '3:30': 'Check Mail',
+      '4:00': 'Team Meeting',
+      '4:30': 'Development',
+      '5:00': 'Development',
+    },
+    {
+      employeeID: 10010,
+      employeeName: 'Michael',
+      '9:00': 'Task Assign',
+      '9:30': 'Task Assign',
+      '10:00': 'Task Assign',
+      '10:30': 'Analysis Tasks',
+      '11:00': 'Analysis Tasks',
+      '11:30': 'Development',
+      '12:00': 'Development',
+      '12:30': 'Development',
+      '1:00': 'Lunch Break',
+      '1:30': 'Lunch Break',
+      '2:00': 'Lunch Break',
+      '2:30': 'Testing',
+      '3:00': 'Testing',
+      '3:30': 'Testing',
+      '4:00': 'Build',
+      '4:30': 'Build',
+      '5:00': 'Build',
+    },
+  ];
 }
 
 function toggleSubTitle() {
@@ -96,11 +402,15 @@ function toggleSubTitle() {
   const action = showSubTitle.value ? 'remove' : 'add';
   document.querySelector('.subtitle')?.classList[action]('hidden');
 }
+
+function vueGridReady(grid: SlickgridVueInstance) {
+  vueGrid = grid;
+}
 </script>
 
 <template>
   <h2>
-    Example 43: Dynamically Create Grid from CSV / Excel import
+    Example 43: colspan/rowspan - Employees Timesheets
     <span class="float-end">
       <a
         style="font-size: 18px"
@@ -116,42 +426,104 @@ function toggleSubTitle() {
   </h2>
 
   <div class="subtitle">
-    Allow creating a grid dynamically by importing an external CSV or Excel file. This script demo will read the CSV file and will consider
-    the first row as the column header and create the column definitions accordingly, while the next few rows will be considered the
-    dataset. Note that this example is demoing a CSV file import but in your application you could easily implemnt an Excel file uploading.
+    <p class="italic example-details">
+      <b>NOTES</b>: <code>rowspan</code> is an opt-in feature, because of its small perf hit (it needs to loop through all row metadatas to
+      map all rowspan), and requires the <code>enableCellRowSpan</code> grid option to be enabled to work properly. The
+      <code>colspan</code>/<code>rowspan</code> are both using DataView item metadata and are both based on row indexes and will
+      <b>not</b> keep the row in sync with the data. It is really up to you the user to update the metadata logic of how and where the cells
+      should span when a side effect kicks in. (i.e: Filtering/Sorting/Paging/Column Reorder... will <b>not</b> change/update the spanning
+      in the grid by itself and that is why they these features are all disabled in this example). Also, column/row freezing (pinning) are
+      also not supported, or at least not recommended unless you know exactly what you're doing (like in this demo here because we know our
+      pinning doesn't intersect)! Any freezing column/row that could intersect with a <code>colspan</code>/<code>rowspan</code>
+      <b>will cause problems</b>.
+    </p>
   </div>
 
-  <div>A default CSV file can be download <a id="template-dl" :href="templateUrl">here</a>.</div>
-
-  <div class="d-flex mt-5 align-items-end">
-    <div class="file-upload">
-      <label for="formFile" class="form-label">Choose a CSV file…</label>
-      <input class="form-control" type="file" data-test="file-upload-input" :value="uploadFileRef" @change="handleFileImport" />
-    </div>
-    <span class="mx-3">or</span>
-    <div>
-      <button id="uploadBtn" data-test="static-data-btn" class="btn btn-outline-secondary" @click="handleDefaultCsv">
-        Use default CSV data
-      </button>
-      &nbsp;/
-      <button class="btn btn-outline-danger btn-sm ms-2" @click="disposeGrid()">Destroy Grid</button>
-    </div>
-  </div>
+  <button
+    class="ms-2 btn btn-outline-secondary btn-sm btn-icon"
+    data-test="goto-up"
+    @click="navigateUp()"
+    title="from an active cell, navigate to cell above"
+  >
+    <span class="mdi mdi-chevron-down mdi-rotate-180"></span>
+    Navigate Up Cell
+  </button>
+  <button
+    class="ms-2 btn btn-outline-secondary btn-sm btn-icon"
+    data-test="goto-down"
+    @click="navigateDown()"
+    title="from an active cell, navigate to cell below"
+  >
+    <span class="mdi mdi-chevron-down"></span>
+    Navigate Down Cell
+  </button>
+  <button
+    class="ms-2 btn btn-outline-secondary btn-sm btn-icon"
+    data-test="goto-prev"
+    @click="navigatePrev()"
+    title="from an active cell, navigate to previous left cell"
+  >
+    <span class="mdi mdi-chevron-down mdi-rotate-90"></span>
+    Navigate to Left Cell
+  </button>
+  <button
+    class="ms-2 btn btn-outline-secondary btn-sm btn-icon"
+    data-test="goto-next"
+    @click="navigateNext()"
+    title="from an active cell, navigate to next right cell"
+  >
+    <span class="mdi mdi-chevron-down mdi-rotate-270"></span>
+    Navigate to Right Cell
+  </button>
+  <button class="ms-2 btn btn-outline-secondary btn-sm btn-icon mx-1" @click="toggleEditing()" data-test="toggle-editing">
+    <span class="mdi mdi-pencil-outline"></span>
+    <span
+      >Toggle Editing: <span id="isEditable" class="text-italic">{{ isEditable }}</span></span
+    >
+  </button>
 
-  <hr />
+  <div class="grid-container grid43-container">
+    <div class="grid43"></div>
+  </div>
 
   <slickgrid-vue
-    v-if="gridCreated"
     v-model:options="gridOptions"
     v-model:columns="columnDefinitions as Column[]"
     v-model:data="dataset"
     grid-id="grid43"
+    @onVueGridCreated="vueGridReady($event.detail)"
   >
   </slickgrid-vue>
 </template>
 
 <style lang="scss">
-.file-upload {
-  max-width: 300px;
+#grid43 {
+  --slick-border-color: #d4d4d4;
+  // --slick-cell-border-left: 1px solid var(--slick-border-color);
+  --slick-header-menu-display: none;
+  --slick-header-column-height: 20px;
+  --slick-grid-border-color: #d4d4d4;
+  --slick-row-selected-color: #d4ebfd;
+  --slick-cell-active-box-shadow: inset 0 0 0 1px #3ca4ff;
+
+  --slick-row-mouse-hover-box-shadow: 0;
+  --slick-cell-odd-background-color: #fff;
+  // --slick-cell-border-top: 0;
+  --slick-cell-border-right: 1px solid var(--slick-border-color);
+  --slick-cell-border-bottom: 1px solid var(--slick-border-color);
+  // --slick-cell-border-left: 1px;
+  --slick-cell-box-shadow: none;
+  --slick-row-mouse-hover-color: #fff;
+  --slick-cell-display: flex;
+
+  .slick-cell.rowspan {
+    // background: white;
+    z-index: 9;
+  }
+  .slick-cell {
+    display: flex;
+    align-items: center;
+    /* justify-content: center; */
+  }
 }
 </style>
diff --git a/demos/vue/src/components/Example44.vue b/demos/vue/src/components/Example44.vue
new file mode 100644
index 000000000..a56eb6a28
--- /dev/null
+++ b/demos/vue/src/components/Example44.vue
@@ -0,0 +1,348 @@
+<script setup lang="ts">
+import { type Column, type Formatter, type GridOption, type ItemMetadata, SlickgridVue, type SlickgridVueInstance } from 'slickgrid-vue';
+import { onBeforeMount, ref } from 'vue';
+
+const gridOptions = ref<GridOption>();
+const columnDefinitions = ref<Column[]>([]);
+let vueGrid!: SlickgridVueInstance;
+const dataLn = ref<number | string>('loading...');
+const scrollToRow = ref(100);
+const dataset = ref<any[]>([]);
+const showSubTitle = ref(true);
+const metadata: Record<number, ItemMetadata> = {
+  0: {
+    columns: {
+      1: { rowspan: 3 },
+    },
+  },
+  2: {
+    columns: {
+      0: { rowspan: 3 },
+      3: { colspan: 3 },
+    },
+  },
+  3: {
+    columns: {
+      1: { rowspan: 5, colspan: 1, cssClass: 'cell-var-span' },
+      // 1: { rowspan: 3, colspan: 2, cssClass: "cell-var-span" },
+      3: { rowspan: 3, colspan: 5 },
+    },
+  },
+  8: {
+    columns: {
+      1: { rowspan: 80 },
+      3: { rowspan: 1999, colspan: 2, cssClass: 'cell-very-high' },
+    },
+  },
+  12: {
+    columns: {
+      11: { rowspan: 3 },
+    },
+  },
+  15: {
+    columns: {
+      18: { colspan: 4, rowspan: 3 },
+    },
+  },
+  85: {
+    columns: {
+      5: { rowspan: 20 },
+    },
+  },
+};
+const rowCellValueFormatter: Formatter = (row, cell, value) => {
+  return `<div class="cellValue">${value.toFixed(2)}</div><div class="valueComment">${row}.${cell}</div>`;
+};
+
+onBeforeMount(() => {
+  defineGrid();
+  // mock some data (different in each dataset)
+  loadData(500);
+});
+
+/* Define grid Options and Columns */
+function defineGrid() {
+  columnDefinitions.value = [
+    { id: 'title', name: 'Title', field: 'title', minWidth: 80 },
+    { id: 'revenueGrowth', name: 'Revenue Growth', field: 'revenueGrowth', formatter: rowCellValueFormatter, minWidth: 120 },
+    {
+      id: 'pricingPolicy',
+      name: 'Pricing Policy',
+      field: 'pricingPolicy',
+      minWidth: 110,
+      sortable: true,
+      formatter: rowCellValueFormatter,
+    },
+    { id: 'policyIndex', name: 'Policy Index', field: 'policyIndex', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'expenseControl', name: 'Expense Control', field: 'expenseControl', minWidth: 110, formatter: rowCellValueFormatter },
+    { id: 'excessCash', name: 'Excess Cash', field: 'excessCash', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'netTradeCycle', name: 'Net Trade Cycle', field: 'netTradeCycle', minWidth: 110, formatter: rowCellValueFormatter },
+    { id: 'costCapital', name: 'Cost of Capital', field: 'costCapital', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'revenueGrowth2', name: 'Revenue Growth', field: 'revenueGrowth2', formatter: rowCellValueFormatter, minWidth: 120 },
+    {
+      id: 'pricingPolicy2',
+      name: 'Pricing Policy',
+      field: 'pricingPolicy2',
+      minWidth: 110,
+      sortable: true,
+      formatter: rowCellValueFormatter,
+    },
+    { id: 'policyIndex2', name: 'Policy Index', field: 'policyIndex2', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'expenseControl2', name: 'Expense Control', field: 'expenseControl2', minWidth: 110, formatter: rowCellValueFormatter },
+    { id: 'excessCash2', name: 'Excess Cash', field: 'excessCash2', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'netTradeCycle2', name: 'Net Trade Cycle', field: 'netTradeCycle2', minWidth: 110, formatter: rowCellValueFormatter },
+    { id: 'costCapital2', name: 'Cost of Capital', field: 'costCapital2', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'revenueGrowth3', name: 'Revenue Growth', field: 'revenueGrowth3', formatter: rowCellValueFormatter, minWidth: 120 },
+    {
+      id: 'pricingPolicy3',
+      name: 'Pricing Policy',
+      field: 'pricingPolicy3',
+      minWidth: 110,
+      sortable: true,
+      formatter: rowCellValueFormatter,
+    },
+    { id: 'policyIndex3', name: 'Policy Index', field: 'policyIndex3', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'expenseControl3', name: 'Expense Control', field: 'expenseControl3', minWidth: 110, formatter: rowCellValueFormatter },
+    { id: 'excessCash3', name: 'Excess Cash', field: 'excessCash3', minWidth: 100, formatter: rowCellValueFormatter },
+    { id: 'netTradeCycle3', name: 'Net Trade Cycle', field: 'netTradeCycle3', minWidth: 110, formatter: rowCellValueFormatter },
+    { id: 'costCapital3', name: 'Cost of Capital', field: 'costCapital3', minWidth: 100, formatter: rowCellValueFormatter },
+  ];
+
+  gridOptions.value = {
+    enableCellNavigation: true,
+    enableColumnReorder: true,
+    enableCellRowSpan: true,
+    gridHeight: 600,
+    gridWidth: 900,
+    rowHeight: 30,
+    dataView: {
+      globalItemMetadataProvider: {
+        getRowMetadata: (item: any, row: any) => renderDifferentColspan(item, row),
+      },
+    },
+    rowTopOffsetRenderType: 'top', // rowspan doesn't render well with 'transform', default is 'top'
+  };
+}
+
+function clearScrollTo() {
+  scrollToRow.value = 0;
+  document.querySelector<HTMLInputElement>('#nRow')?.focus();
+}
+
+function loadData(count: number) {
+  dataLn.value = 'loading...';
+
+  // add a delay just to show the "loading" text before it loads all data
+  setTimeout(() => {
+    // mock data
+    const tmpArray: any[] = [];
+    for (let i = 0; i < count; i++) {
+      tmpArray[i] = {
+        id: i,
+        title: 'Task ' + i,
+        revenueGrowth: Math.random() * Math.pow(10, Math.random() * 3),
+        pricingPolicy: Math.random() * Math.pow(10, Math.random() * 3),
+        policyIndex: Math.random() * Math.pow(10, Math.random() * 3),
+        expenseControl: Math.random() * Math.pow(10, Math.random() * 3),
+        excessCash: Math.random() * Math.pow(10, Math.random() * 3),
+        netTradeCycle: Math.random() * Math.pow(10, Math.random() * 3),
+        costCapital: Math.random() * Math.pow(10, Math.random() * 3),
+        revenueGrowth2: Math.random() * Math.pow(10, Math.random() * 3),
+        pricingPolicy2: Math.random() * Math.pow(10, Math.random() * 3),
+        policyIndex2: Math.random() * Math.pow(10, Math.random() * 3),
+        expenseControl2: Math.random() * Math.pow(10, Math.random() * 3),
+        excessCash2: Math.random() * Math.pow(10, Math.random() * 3),
+        netTradeCycle2: Math.random() * Math.pow(10, Math.random() * 3),
+        costCapital2: Math.random() * Math.pow(10, Math.random() * 3),
+        revenueGrowth3: Math.random() * Math.pow(10, Math.random() * 3),
+        pricingPolicy3: Math.random() * Math.pow(10, Math.random() * 3),
+        policyIndex3: Math.random() * Math.pow(10, Math.random() * 3),
+        expenseControl3: Math.random() * Math.pow(10, Math.random() * 3),
+        excessCash3: Math.random() * Math.pow(10, Math.random() * 3),
+        netTradeCycle3: Math.random() * Math.pow(10, Math.random() * 3),
+        costCapital3: Math.random() * Math.pow(10, Math.random() * 3),
+      };
+    }
+
+    // let's keep column 3-4 as the row spanning from row 8 until the end of the grid
+    metadata[8].columns![3].rowspan = tmpArray.length - 8;
+
+    vueGrid?.dataView?.beginUpdate();
+    vueGrid?.dataView?.setItems(tmpArray);
+    vueGrid?.dataView?.endUpdate();
+    dataLn.value = count;
+  }, 20);
+}
+
+/**
+ * A callback to render different row column span
+ * Your callback will always have the "item" argument which you can use to decide on the colspan
+ * Your return object must always be in the form of:: { columns: { [columnName]: { colspan: number|'*' } }}
+ */
+function renderDifferentColspan(_item: any, row: any): any {
+  return (metadata[row] as ItemMetadata)?.attributes
+    ? metadata[row]
+    : (metadata[row] = { attributes: { 'data-row': row }, ...metadata[row] });
+}
+
+function handleToggleSpans() {
+  const cell = metadata[3].columns![1];
+  if (cell.colspan === 1) {
+    cell.rowspan = 3;
+    cell.colspan = 2;
+  } else {
+    cell.rowspan = 5;
+    cell.colspan = 1;
+  }
+
+  // row index 3 can have a rowspan of up to 5 rows, so we need to invalidate from row 3 + 5 (-1 because of zero index)
+  // so: 3 + 5 - 1 => row indexes 3 to 7
+  vueGrid.slickGrid?.invalidateRows([3, 4, 5, 6, 7]);
+  vueGrid.slickGrid?.render();
+}
+
+function handleScrollTo() {
+  // const args = event.detail && event.detail.args;
+  vueGrid.slickGrid?.scrollRowToTop(scrollToRow.value);
+  return false;
+}
+
+function toggleSubTitle() {
+  showSubTitle.value = !showSubTitle.value;
+  const action = showSubTitle.value ? 'remove' : 'add';
+  document.querySelector('.subtitle')?.classList[action]('hidden');
+}
+
+function vueGridReady(grid: SlickgridVueInstance) {
+  vueGrid = grid;
+}
+</script>
+
+<template>
+  <h2>
+    Example 44: colspan/rowspan with large dataset
+    <span class="float-end">
+      <a
+        style="font-size: 18px"
+        target="_blank"
+        href="https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vue/src/components/Example44.vue"
+      >
+        <span class="mdi mdi-link-variant"></span> code
+      </a>
+    </span>
+    <button class="ms-2 btn btn-outline-secondary btn-sm btn-icon" type="button" data-test="toggle-subtitle" @click="toggleSubTitle()">
+      <span class="mdi mdi-information-outline" title="Toggle example sub-title details"></span>
+    </button>
+  </h2>
+
+  <div class="subtitle">
+    <p class="italic example-details">
+      This page demonstrates <code>colspan</code> & <code>rowspan</code> using DataView with item metadata. <b>Note</b>:
+      <code>colspan</code> & <code>rowspan</code> are rendered via row/cell indexes, any operations that could change these indexes (i.e.
+      Filtering/Sorting/Paging/Column Reorder) will require you to implement proper logic to recalculate these indexes (it becomes your
+      responsability). This demo does not show this because it is up to you to decide what to do when the span changes shape (i.e. you
+      default to 3 rowspan but you filter a row in the middle, how do you want to proceed?).
+    </p>
+  </div>
+
+  <div class="row">
+    <div class="col">
+      <button class="ms-1 btn btn-outline-secondary btn-sm" data-test="add-500-rows-btn" @click="loadData(500)">500 rows</button>
+      <button class="ms-1 btn btn-outline-secondary btn-sm" data-test="add-5k-rows-btn" @click="loadData(5000)">5k rows</button>
+      <button class="ms-1 btn btn-outline-secondary btn-sm" data-test="add-50k-rows-btn" @click="loadData(50000)">50k rows</button>
+      <button class="mx-1 btn btn-outline-secondary btn-sm" data-test="add-50k-rows-btn" @click="loadData(500000)">500k rows</button>
+      <label>data length: </label><span id="dataLn" :textcontent="dataLn"></span>
+      <button
+        id="toggleSpans"
+        class="ms-1 btn btn-outline-secondary btn-sm btn-icon mx-1"
+        @click="handleToggleSpans()"
+        data-test="toggleSpans"
+      >
+        <span class="mdi mdi-flip-vertical"></span>
+        <span>Toggle blue cell colspan &amp; rowspan</span>
+      </button>
+      <button id="scrollTo" class="ms-1 btn btn-outline-secondary btn-sm btn-icon" @click="handleScrollTo()" data-test="scrollToBtn">
+        <span class="mdi mdi-arrow-down"></span>
+        <span>Scroll To Row</span>
+      </button>
+    </div>
+    <div class="col">
+      <div class="input-group input-group-sm" style="width: 100px">
+        <input
+          v-model="scrollToRow"
+          id="nRow"
+          type="text"
+          data-test="nbrows"
+          class="form-control search-string"
+          placeholder="search value"
+          autocomplete="off"
+        />
+        <button class="btn btn-sm btn-outline-secondary d-flex align-items-center" data-test="clearScrollTo" @click="clearScrollTo()">
+          <span class="icon mdi mdi-close-thick"></span>
+        </button>
+      </div>
+    </div>
+  </div>
+
+  <slickgrid-vue
+    v-model:options="gridOptions"
+    v-model:columns="columnDefinitions as Column[]"
+    v-model:data="dataset"
+    grid-id="grid44"
+    @onVueGridCreated="vueGridReady($event.detail)"
+  >
+  </slickgrid-vue>
+</template>
+
+<style lang="scss">
+#grid44 {
+  --slick-cell-active-box-shadow: inset 0 0 0 1px #e35ddc;
+
+  .slick-row.even .slick-cell.cell-very-high {
+    background-color: #f0ffe0;
+  }
+  .slick-row.odd .slick-cell.cell-var-span {
+    background-color: #87ceeb;
+  }
+  .slick-row .slick-cell.rowspan {
+    background-color: #95b7a2;
+    z-index: 10;
+  }
+  .slick-row[data-row='3'] .slick-cell.l3.rowspan {
+    background-color: #95b7a2;
+  }
+  .slick-row[data-row='2'] .slick-cell.l3.r5 {
+    background-color: #ddfffc;
+  }
+  .slick-row[data-row='0'] .slick-cell.rowspan,
+  .slick-row[data-row='8'] .slick-cell.rowspan {
+    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQ0lEQVQYV2N8/fr1fwY84M6dOwyM+BSBFKioqOBWBFMAsgSrScgKsCpCV4ChCJsCFEW4FMAV4VMAVnT8+PH/IG/iAwDA1DlezHn8bwAAAABJRU5ErkJggg==);
+  }
+  .slick-row[data-row='8'] .slick-cell.rowspan:nth-child(4) {
+    background: #f0ffe0;
+  }
+  .slick-row[data-row='12'] .slick-cell.rowspan {
+    background: #bd8b8b;
+  }
+  .slick-row[data-row='15'] .slick-cell.rowspan {
+    background: #edc12e;
+  }
+  .slick-row[data-row='85'] .slick-cell.rowspan {
+    background: #8baebd;
+  }
+  .slick-cell.active {
+    /* use a different active cell color to make it a bit more obvious */
+    box-shadow: inset 0 0 0 1px #e35ddc;
+  }
+  .cellValue {
+    float: right;
+    font-size: 14px;
+  }
+  .valueComment {
+    color: #7c8983;
+    font-size: 12px;
+    font-style: italic;
+    width: fit-content;
+  }
+}
+</style>
diff --git a/demos/vue/src/router/index.ts b/demos/vue/src/router/index.ts
index d64df5356..cdf01ebeb 100644
--- a/demos/vue/src/router/index.ts
+++ b/demos/vue/src/router/index.ts
@@ -17,6 +17,7 @@ import Example13 from '../components/Example13.vue';
 import Example14 from '../components/Example14.vue';
 import Example15 from '../components/Example15.vue';
 import Example16 from '../components/Example16.vue';
+import Example17 from '../components/Example17.vue';
 import Example18 from '../components/Example18.vue';
 import Example19 from '../components/Example19.vue';
 import Example20 from '../components/Example20.vue';
@@ -43,6 +44,7 @@ import Example40 from '../components/Example40.vue';
 import Example41 from '../components/Example41.vue';
 import Example42 from '../components/Example42.vue';
 import Example43 from '../components/Example43.vue';
+import Example44 from '../components/Example44.vue';
 import Home from '../Home.vue';
 
 export const routes: RouteRecordRaw[] = [
@@ -64,6 +66,7 @@ export const routes: RouteRecordRaw[] = [
   { path: '/example14', name: '14- Column Span & Header Grouping', component: Example14 },
   { path: '/example15', name: '15- Grid State & Local Storage', component: Example15 },
   { path: '/example16', name: '16- Row Move Plugin', component: Example16 },
+  { path: '/example17', name: '17- Create Grid from CSV', component: Example17 },
   { path: '/example18', name: '18- Draggable Grouping', component: Example18 },
   { path: '/example19', name: '19- Row Detail View', component: Example19 },
   { path: '/example20', name: '20- Pinned Columns / Rows', component: Example20 },
@@ -89,7 +92,8 @@ export const routes: RouteRecordRaw[] = [
   { path: '/example40', name: '40- Infinite Scroll from JSON data', component: Example40 },
   { path: '/example41', name: '41- Drag & Drop', component: Example41 },
   { path: '/example42', name: '42- Custom Pagination', component: Example42 },
-  { path: '/example43', name: '43- Create Grid from CSV', component: Example43 },
+  { path: '/example43', name: '43- Colspan/Rowspan (timesheets)', component: Example43 },
+  { path: '/example44', name: '44- Colspan/Rowspan (large data)', component: Example44 },
 ];
 
 export const router = createRouter({
diff --git a/demos/vue/test/cypress/e2e/example17.cy.ts b/demos/vue/test/cypress/e2e/example17.cy.ts
new file mode 100644
index 000000000..619d7842f
--- /dev/null
+++ b/demos/vue/test/cypress/e2e/example17.cy.ts
@@ -0,0 +1,83 @@
+describe('Example 43 - Dynamically Create Grid from CSV / Excel import', () => {
+  const defaultCsvTitles = ['First Name', 'Last Name', 'Age', 'Type'];
+  const GRID_ROW_HEIGHT = 33;
+
+  it('should display Example title', () => {
+    cy.visit(`${Cypress.config('baseUrl')}/example43`);
+    cy.get('h2').should('contain', 'Example 43: Dynamically Create Grid from CSV / Excel import');
+  });
+
+  it('should load default CSV file and expect default column titles', () => {
+    cy.get('[data-test="static-data-btn"]').click();
+
+    cy.get('.slick-header-columns')
+      .children()
+      .each(($child, index) => expect($child.text()).to.eq(defaultCsvTitles[index]));
+  });
+
+  it('should expect default data in the grid', () => {
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '20');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '21');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+  });
+
+  it('should sort by "Age" and expect it to be sorted in ascending order', () => {
+    cy.get('.slick-header-columns .slick-header-column:nth(2)').click();
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '20');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '33');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
+  });
+
+  it('should click again the "Age" column and expect it to be sorted in descending order', () => {
+    cy.get('.slick-header-columns .slick-header-column:nth(2)').click();
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '20');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+  });
+
+  it('should filter Smith as "Last Name" and expect only 1 row in the grid', () => {
+    cy.get('.slick-headerrow .slick-headerrow-column:nth(1) input').type('Smith');
+
+    cy.get('.slick-row').should('have.length', 1);
+
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
+    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
+  });
+});
diff --git a/demos/vue/test/cypress/e2e/example43.cy.ts b/demos/vue/test/cypress/e2e/example43.cy.ts
index 619d7842f..727d20e6c 100644
--- a/demos/vue/test/cypress/e2e/example43.cy.ts
+++ b/demos/vue/test/cypress/e2e/example43.cy.ts
@@ -1,83 +1,406 @@
-describe('Example 43 - Dynamically Create Grid from CSV / Excel import', () => {
-  const defaultCsvTitles = ['First Name', 'Last Name', 'Age', 'Type'];
-  const GRID_ROW_HEIGHT = 33;
+describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, () => {
+  const GRID_ROW_HEIGHT = 30;
+  const fullTitles = [
+    'Employee ID',
+    'Employee Name',
+    '9:00 AM',
+    '9:30 AM',
+    '10:00 AM',
+    '10:30 AM',
+    '11:00 AM',
+    '11:30 AM',
+    '12:00 PM',
+    '12:30 PM',
+    '1:00 PM',
+    '1:30 PM',
+    '2:00 PM',
+    '2:30 PM',
+    '3:00 PM',
+    '3:30 PM',
+    '4:00 PM',
+    '4:30 PM',
+    '5:00 PM',
+  ];
 
   it('should display Example title', () => {
     cy.visit(`${Cypress.config('baseUrl')}/example43`);
-    cy.get('h2').should('contain', 'Example 43: Dynamically Create Grid from CSV / Excel import');
+    cy.get('h2').should('contain', 'Example 43: colspan/rowspan - Employees Timesheets');
   });
 
-  it('should load default CSV file and expect default column titles', () => {
-    cy.get('[data-test="static-data-btn"]').click();
-
+  it('should have exact column titles', () => {
     cy.get('.slick-header-columns')
       .children()
-      .each(($child, index) => expect($child.text()).to.eq(defaultCsvTitles[index]));
+      .each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
+  });
+
+  it('should expect 1st column to be frozen (frozen)', () => {
+    cy.get('.grid-canvas-left .slick-cell.frozen').should('have.length', 10);
+    cy.get('.grid-canvas-right .slick-cell:not(.frozen)').should('have.length.above', 60);
   });
 
-  it('should expect default data in the grid', () => {
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
-
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '20');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
-
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '21');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+  describe('Spanning', () => {
+    it('should expect "Davolio", "Check Mail", and "Development" to all have rowspan of 2 in morning hours', () => {
+      cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio');
+      cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2)
+      );
+
+      cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail');
+      cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2)
+      );
+
+      cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should('contain', 'Development');
+      cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2)
+      );
+    });
+
+    it('should expect "Lunch Break" to span over 3 columns and over all rows', () => {
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break');
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10)
+      );
+    });
+
+    it('should expect a large "Development" section that spans over multiple columns & rows in the afternoon', () => {
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development');
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5)
+      );
+    });
   });
 
-  it('should sort by "Age" and expect it to be sorted in ascending order', () => {
-    cy.get('.slick-header-columns .slick-header-column:nth(2)').click();
+  describe('Basic Key Navigations', () => {
+    it('should start at Employee 10001, then type "End" key and expect to be in "Team Meeting" between 4:30-5:00pm', () => {
+      cy.get('[data-row=0] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l0.r0.active').should('contain', '10001');
+      cy.get('@active_cell').type('{end}');
+      cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting');
+    });
+
+    it('should start at Employee 10002, then type "End" key and also expect to be in "Team Meeting" between 4:30-5:00pm', () => {
+      cy.get('[data-row=1] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=1] > .slick-cell.l0.r0.active').should('contain', '10002');
+      cy.get('@active_cell').type('{end}');
+      cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting');
+    });
+
+    it('should start at Employee 10004, then type "ArrowRight" key twice and expect to be in "Check Mail" between 9:00-10:30am', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}');
+      cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail');
+    });
+
+    it('should start at Employee 10004, then type "ArrowRight" key 4x times and expect to be in "Testing" between 11:00-1:00pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}');
+      cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing');
+    });
+
+    it('should start at Employee 10004, then type "ArrowRight" key 5x times and expect to be in "Lunch Break"', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}');
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break');
+    });
+
+    it('should start at Employee 10004, then type "ArrowRight" key 6x times and expect to be in "Development" between 2:30-3:30pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}');
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development');
+    });
+
+    // then rollback by going backward
+    it('should be on Employee 10004 row at previous "Development" cell, then type "ArrowLeft" key once and expect to be in "Lunch Break"', () => {
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click();
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development');
+      cy.get('@active_cell').type('{leftarrow}');
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key once and expect to be in "Conference" between 4:00-5:00pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}');
+      cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 3x times and expect to be back to "Development" between 2:30-3:30pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}{leftarrow}{leftarrow}');
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 4x times and expect to be back to "Lunch Break"', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}');
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}');
+      cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing');
+    });
+
+    // going down
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}');
+      cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}{downarrow}');
+      cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}');
+      cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign');
+    });
 
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '20');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}');
+      cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support');
+    });
 
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+    // going up from inverse
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}');
+      cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign');
+    });
 
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '33');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}');
+      cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}');
+      cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}{uparrow}');
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+    });
   });
 
-  it('should click again the "Age" column and expect it to be sorted in descending order', () => {
-    cy.get('.slick-header-columns .slick-header-column:nth(2)').click();
+  describe('Grid Navigate Functions', () => {
+    it('should start at Employee 10004, then type "Navigate Right" twice and expect to be in "Check Mail" between 9:00-10:30am', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-test="goto-next"]').click().click();
+      cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail');
+    });
+
+    it('should start at Employee 10004, then type "Navigate Right" 4x times and expect to be in "Testing" between 11:00-1:00pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('[data-test="goto-next"]').click().click().click().click();
+      cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing');
+    });
+
+    it('should start at Employee 10004, then type "Navigate Right" 5x times and expect to be in "Lunch Break"', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('[data-test="goto-next"]').click().click().click().click().click();
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break');
+    });
+
+    it('should start at Employee 10004, then type "Navigate Right" 6x times and expect to be in "Development" between 2:30-3:30pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('[data-test="goto-next"]').click().click().click().click().click().click();
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development');
+    });
+
+    // then rollback by going backward
+    it('should be on Employee 10004 row at previous "Development" cell, then type "Navigate Left" once and expect to be in "Lunch Break"', () => {
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click();
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development');
+      cy.get('[data-test="goto-prev"]').click();
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" once and expect to be in "Conference" between 4:00-5:00pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-prev"]').click();
+      cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 3x times and expect to be back to "Development" between 2:30-3:30pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-prev"]').click().click().click();
+      cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 4x times and expect to be back to "Lunch Break"', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-prev"]').click().click().click().click();
+      cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break');
+    });
+
+    it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => {
+      cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004');
+      cy.get('@active_cell').type('{end}');
+      cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-prev"]').click().click().click().click().click();
+      cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing');
+    });
 
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
+    // going down
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click();
+      cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support');
+    });
 
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click().click();
+      cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail');
+    });
 
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '20');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click().click().click();
+      cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click().click().click().click();
+      cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support');
+    });
+
+    // going up from inverse
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click().click().click().click();
+      cy.get('[data-test="goto-up"]').click();
+      cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click().click().click().click();
+      cy.get('[data-test="goto-up"]').click().click();
+      cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click().click().click().click();
+      cy.get('[data-test="goto-up"]').click().click().click();
+      cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support');
+    });
+
+    it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+      cy.get('[data-test="goto-down"]').click().click().click().click();
+      cy.get('[data-test="goto-up"]').click().click().click().click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting');
+    });
   });
 
-  it('should filter Smith as "Last Name" and expect only 1 row in the grid', () => {
-    cy.get('.slick-headerrow .slick-headerrow-column:nth(1) input').type('Smith');
+  describe('Grid Editing', () => {
+    it('should toggle editing', () => {
+      cy.get('#isEditable').contains('false');
+      cy.get('[data-row=0] > .slick-cell.l4.r4').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active .editor-text').should('not.exist');
+
+      cy.get('[data-test=toggle-editing]').click();
+      cy.get('#isEditable').contains('true');
+
+      cy.get('[data-row=0] > .slick-cell.l4.r4').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').should('exist');
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').type('Team Meeting.xyz{enter}');
+    });
+
+    // going down
+    it('should have changed active cell to "Support" between 9:30-11:00am', () => {
+      cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text')
+        .invoke('val')
+        .then((text) => expect(text).to.eq('Support'));
+      cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text').type('Support.xyz{enter}');
+    });
+
+    it('should have changed active cell to "Check Email" between 9:00-10:30am', () => {
+      cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text')
+        .invoke('val')
+        .then((text) => expect(text).to.eq('Check Mail'));
+      cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text').type('Check Mail.xyz{enter}');
+    });
+
+    it('should have changed active cell to "Task Assign" between 9:00-11:00am', () => {
+      cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text')
+        .invoke('val')
+        .then((text) => expect(text).to.eq('Task Assign'));
+      cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text').type('Task Assign.xyz{enter}');
+    });
 
-    cy.get('.slick-row').should('have.length', 1);
+    it('should have changed active cell to "Support" between 10:00-11:30am', () => {
+      cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text')
+        .invoke('val')
+        .then((text) => expect(text).to.eq('Support'));
+      cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text').type('Support.xyz{enter}');
+    });
 
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
-    cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
+    it('should have changed active cell to "Testing" and cancel editing when typing "Escape" key', () => {
+      cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text')
+        .invoke('val')
+        .then((text) => expect(text).to.eq('Testing'));
+      cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').type('{esc}');
+      cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').should('not.exist');
+    });
   });
 });
diff --git a/demos/vue/test/cypress/e2e/example44.cy.ts b/demos/vue/test/cypress/e2e/example44.cy.ts
new file mode 100644
index 000000000..d7b64aaec
--- /dev/null
+++ b/demos/vue/test/cypress/e2e/example44.cy.ts
@@ -0,0 +1,458 @@
+describe('Example 44 - Column & Row Span', { retries: 0 }, () => {
+  const GRID_ROW_HEIGHT = 30;
+  const fullTitles = [
+    'Title',
+    'Revenue Growth',
+    'Pricing Policy',
+    'Policy Index',
+    'Expense Control',
+    'Excess Cash',
+    'Net Trade Cycle',
+    'Cost of Capital',
+    'Revenue Growth',
+    'Pricing Policy',
+    'Policy Index',
+    'Expense Control',
+    'Excess Cash',
+    'Net Trade Cycle',
+    'Cost of Capital',
+    'Revenue Growth',
+    'Pricing Policy',
+    'Policy Index',
+    'Expense Control',
+    'Excess Cash',
+    'Net Trade Cycle',
+    'Cost of Capital',
+  ];
+
+  it('should display Example title', () => {
+    cy.visit(`${Cypress.config('baseUrl')}/example44`);
+    cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset');
+  });
+
+  it('should have exact column titles', () => {
+    cy.get('.slick-header-columns')
+      .children()
+      .each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
+  });
+
+  it('should drag Title column to swap with 2nd column "Revenue Growth" in the grid and expect rowspan to stay at same position with Task 0 to spread instead', () => {
+    const expectedTitles = ['Revenue Growth', 'Title', 'Pricing Policy'];
+
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0.rowspan`).should(($el) =>
+      expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+    );
+
+    cy.get('.slick-header-columns').children('.slick-header-column:nth(0)').contains('Title').drag('.slick-header-column:nth(1)');
+    cy.get('.slick-header-column:nth(0)').contains('Revenue Growth');
+    cy.get('.slick-header-column:nth(1)').contains('Title');
+
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).should('contain', 'Task 0');
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l1.r1`).should('not.exist');
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1`).should('contain', 'Task 3');
+
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) =>
+      expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+    );
+
+    cy.get('.slick-header-columns')
+      .children()
+      .each(($child, index) => {
+        if (index < expectedTitles.length) {
+          expect($child.text()).to.eq(expectedTitles[index]);
+        }
+      });
+  });
+
+  it('should drag back Title column to reswap with 2nd column "Revenue Growth" in the grid and expect rowspan to stay at same position with Revenue Growth to now spread', () => {
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) =>
+      expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+    );
+    cy.get('.slick-header-columns').children('.slick-header-column:nth(0)').contains('Revenue Growth').drag('.slick-header-column:nth(1)');
+    cy.get('.slick-header-column:nth(0)').contains('Title');
+    cy.get('.slick-header-column:nth(1)').contains('Revenue Growth');
+
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 0');
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 1');
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 2');
+    cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l0.r0`).should('not.exist');
+
+    const expectedTitles = ['Title', 'Revenue Growth', 'Pricing Policy'];
+    cy.get('.slick-header-columns')
+      .children()
+      .each(($child, index) => {
+        if (index < expectedTitles.length) {
+          expect($child.text()).to.eq(expectedTitles[index]);
+        }
+      });
+  });
+
+  describe('spanning', () => {
+    it('should expect first row to be regular rows without any spanning', () => {
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 0');
+
+      for (let i = 2; i <= 6; i++) {
+        cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l${i}.r${i}`).should('exist');
+      }
+    });
+
+    it('should expect 1st row, second cell to span (rowspan) across 3 rows', () => {
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 0');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1).rowspan`).should(($el) => {
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3);
+      });
+
+      for (let i = 2; i <= 14; i++) {
+        cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(${i})`).contains(/\d+$/); // use regexp to make sure it's a number
+      }
+    });
+
+    it('should expect 3rd row first cell to span (rowspan) across 3 rows', () => {
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0.rowspan`).should('contain', 'Task 2');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+      );
+
+      for (let i = 2; i <= 5; i++) {
+        cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(${i})`).contains(/\d+$/);
+      }
+    });
+
+    it('should expect 4th row to have 2 sections (blue, green) spanning across 3 rows (rowspan) and 2 columns (colspan)', () => {
+      // blue rowspan section
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5)
+      );
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l2.r2`)
+        .should('exist')
+        .contains(/\d+$/);
+
+      // green colspan/rowspan section
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l3.r7`)
+        .should('exist')
+        .contains(/\d+$/);
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l8.r8`)
+        .should('exist')
+        .contains(/\d+$/);
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l9.r9`)
+        .should('exist')
+        .contains(/\d+$/);
+    });
+
+    it('should click on "Toggle blue cell colspan..." and expect colspan to widen from 1 column to 2 columns and from 5 rows to 3 rowspan', () => {
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1.rowspan`).should('exist');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5)
+      );
+
+      cy.get('[data-test="toggleSpans"]').click();
+      cy.get('.slick-cell.l1.r1.rowspan').should('exist');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r2.rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+      );
+    });
+
+    it('should expect Task 8 on 2nd column to have rowspan spanning 80 cells', () => {
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 8');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1).rowspan`).contains(/\d+$/);
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1).rowspan`).should(($el) => {
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80);
+      });
+    });
+
+    it('should scroll to the right and still expect spans without any extra texts', () => {
+      cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(400, 0).wait(10);
+
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).contains(/\d+$/);
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0).rowspan`).should('exist');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1).rowspan`).should('exist');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1).rowspan`).should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+      );
+
+      // next rows are regular cells
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell.l3.r3`).should('not.exist');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell.l4.r4`).should('not.exist');
+
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell.l3.r3`).should('not.exist');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell.l3.r3`).should('not.exist');
+
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell.l4.r4`).should('exist');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell.l4.r4`).should('exist');
+    });
+
+    it('should scroll back to left and expect Task 8 to have 2 different spans (Revenue Grow: rowspan=80, Policy Index: rowspan=2000,colspan=2)', () => {
+      cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).wait(10);
+
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 8');
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1).rowspan`).should(($el) => {
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80);
+      });
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1)`).contains(/\d+$/);
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(2)`).contains(/\d+$/);
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell.l3.r4`).should('exist');
+
+      cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 9}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 9');
+    });
+
+    it('should scroll to row 85 and still expect 3 spans in the screen, "Revenue Growth" and "Policy Index" spans', () => {
+      cy.get('[data-test="clearScrollTo"]').click();
+      cy.get('[data-test="nbrows"]').type('{backspace}85');
+      cy.get('[data-test="scrollToBtn"]').click();
+
+      // left dashed rowspan "Revenue Growth"
+      cy.get(`[data-row=85] > .slick-cell.l0.r0`).should('contain', 'Task 85');
+      cy.get(`[data-row=85] > .slick-cell.l2.r2`).contains(/\d+$/);
+
+      // rowspan middle (yellowish) "Policy Index"
+      cy.get(`[data-row=88] > .slick-cell.l0.r0`).should('contain', 'Task 88');
+      cy.get(`[data-row=88] > .slick-cell.l1.r1`).should('exist');
+    });
+
+    it('should scroll to the end of the grid and still expect "PolicyIndex" column to span across 2 columns and rows until the end of the grid', () => {
+      cy.get('[data-test="clearScrollTo"]').click();
+      cy.get('[data-test="nbrows"]').type('{backspace}490');
+      cy.get('[data-test="scrollToBtn"]').click();
+
+      cy.get(`[data-row=485] > .slick-cell.l0.r0`).should('contain', 'Task 485');
+
+      cy.get(`[data-row=499] > .slick-cell.l0.r0`).should('contain', 'Task 499');
+      cy.get(`[data-row=499] > .slick-cell.l1.r1`).should('exist');
+      cy.get(`[data-row=499] > .slick-cell.l2.r2`).should('exist');
+      cy.get(`[data-row=499] > .slick-cell.l5.r5`).should('exist');
+    });
+
+    it('should load 5K data and expect 8.3 rowspan row height to increase/decrease when data changes from 500 to 5K back to 500', () => {
+      cy.get('[data-row=8] .slick-cell.l3.r4').should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * (500 - 8))
+      );
+
+      cy.get('[data-test="add-5k-rows-btn"]').click();
+      cy.get('[data-row=8] .slick-cell.l3.r4').should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * (5000 - 8))
+      );
+
+      cy.get('[data-test="add-500-rows-btn"]').click();
+      cy.get('[data-row=8] .slick-cell.l3.r4').should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * (500 - 8))
+      );
+    });
+  });
+
+  describe('Basic Key Navigations', () => {
+    it('should scroll back to top', () => {
+      cy.get('[data-test="clearScrollTo"]').click();
+      cy.get('[data-test="nbrows"]').type('0');
+      cy.get('[data-test="scrollToBtn"]').click();
+    });
+
+    it('should start at Task 6 on PolicyIndex column, then type "Arrow Up" key and expect active cell to become the green section in the middle', () => {
+      cy.get('[data-row=6] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('[data-row=6] > .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('@active_cell').type('{uparrow}');
+      cy.get('[data-row=3] > .slick-cell.l1.r2.active').should('have.length', 1);
+    });
+
+    it('should start at Task 6 on PricingPolicy column, then type "Arrow Left" key and expect active cell to become the green section in the middle', () => {
+      cy.get('[data-row=6] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('[data-row=6] > .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('[data-test="toggleSpans"]').click();
+      cy.get('@active_cell').type('{leftarrow}');
+      cy.get('[data-row=3] .slick-cell.l1.r1.active').should('have.length', 1);
+    });
+
+    it('should start at Task 5 on Task 5 column, then type "Arrow Right" key 3x times and expect active cell to become the wide green section in the middle', () => {
+      cy.get('[data-row=5] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1);
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}');
+      cy.get('[data-row=5] .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('[data-row=5] .slick-cell.l2.r2.active').type('{rightarrow}');
+      cy.get('[data-row=3] .slick-cell.l3.r7.active').should(($el) =>
+        expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)
+      );
+    });
+
+    it('should start at Task 2 on PricingPolicy column, then type "Arrow Left" key and expect active cell to become the dashed section beside Task 0-3 on RevenueGrowth column', () => {
+      cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('[data-row=2] > .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('@active_cell').type('{leftarrow}');
+      cy.get('[data-row=0] > .slick-cell.l1.r1.active').should('have.length', 1);
+    });
+
+    it('should start at Task 2 on PricingPolicy column, then type "Arrow Left" key twice and expect active cell to become Task 2 cell', () => {
+      cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('[data-row=2] > .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('@active_cell').type('{leftarrow}{leftarrow}');
+      cy.get('[data-row=2] > .slick-cell.l0.r0.active').contains('Task 2');
+      cy.get('[data-row=2] > .slick-cell.l0.r0.active').should('have.length', 1);
+    });
+
+    it('should start at Task 2 on PricingPolicy column, then type "Home" key and expect active cell to become Task 2 cell', () => {
+      cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('[data-row=2] .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('@active_cell').type('{home}');
+      cy.get('[data-row=2] .slick-cell.l0.r0.active').contains('Task 2');
+      cy.get('[data-row=2] .slick-cell.l0.r0.active').should('have.length', 1);
+    });
+
+    it('should start at Task 2 on PricingPolicy column, then type "End" key and expect active cell to become Task 2 cell', () => {
+      cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('[data-row=2] .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('@active_cell').type('{end}');
+      cy.get('[data-row=2] .slick-cell.l21.r21.active').should('have.length', 1);
+    });
+
+    it('should start at RevenueGrowth column on first dashed cell, then type "Ctrl+End" then "Ctrl+Home" keys and expect active cell to go to bottom/top of grid on same column', () => {
+      cy.get('[data-row=0] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l2.r2.active').should('have.length', 1);
+      cy.get('@active_cell').type('{ctrl}{end}', { release: false });
+      cy.get('[data-row=499] > .slick-cell.l21.r21.active').should('have.length', 1);
+      cy.get('[data-row=499] > .slick-cell.l21.r21.active').type('{ctrl}{home}', { release: false });
+      cy.get('[data-row=0] .slick-cell.l0.r0.active').should('have.length', 1);
+      cy.get('[data-row=0] .slick-cell.l1.r1').should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3));
+    });
+
+    it('should start at first row on PolicyIndex column, then type "Ctrl+DownArrow" keys and expect active cell to become yellowish section', () => {
+      cy.get('[data-row=0] > .slick-cell.l3.r3').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l3.r3.active').should('have.length', 1);
+      cy.get('@active_cell').type('{ctrl}{downarrow}', { release: false });
+      cy.get('[data-row=8] > .slick-cell.l3.r4.active').should('have.length', 1);
+    });
+
+    it('should start at first row on ExpenseControl column, then type "Ctrl+DownArrow" keys and expect active cell to become the cell just above the yellowish section', () => {
+      cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click();
+      cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('have.length', 1);
+      cy.get('@active_cell').type('{ctrl}{downarrow}', { release: false });
+      cy.get('[data-row=7] .slick-cell.l4.r4.active').should('have.length', 1);
+    });
+
+    it('should start at Task 13, type "End" key and expect active cell to be the last span cell', () => {
+      cy.get('[data-row=13] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=13] > .slick-cell.l0.r0.active').should('have.length', 1);
+      cy.get('@active_cell').type('{end}');
+      cy.get('[data-row=13] > .slick-cell.l21.r21.active').should('have.length', 1);
+    });
+
+    it('should go to Task 13 last cell, type "Home" key and expect active cell to become Task 13', () => {
+      cy.get('[data-row=13] > .slick-cell.l21.r21').as('active_cell').click();
+      cy.get('[data-row=13] > .slick-cell.l21.r21.active').should('have.length', 1);
+      cy.get('@active_cell').type('{home}');
+      cy.get('[data-row=13] > .slick-cell.l0.r0.active').should('have.length', 1);
+    });
+
+    it('should start at Task 15, type "End" key and expect active cell to be on the last orange span cell', () => {
+      cy.get('[data-row=15] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=15] > .slick-cell.l0.r0.active').should('have.length', 1);
+      cy.get('@active_cell').type('{end}');
+      cy.get('[data-row=15] > .slick-cell.l18.r21.active').should('have.length', 1);
+      cy.get('[data-row=15] > .slick-cell.l18.r21.active').type('{home}');
+    });
+
+    it('should start at Task 17, type "End" key and expect active cell to be on the last orange span cell', () => {
+      cy.get('[data-row=17] > .slick-cell.l0.r0').as('active_cell').click();
+      cy.get('[data-row=17] > .slick-cell.l0.r0.active').should('have.length', 1);
+      cy.get('@active_cell').type('{end}');
+      cy.get('[data-row=15] > .slick-cell.l18.r21.active').should('have.length', 1);
+      cy.get('[data-row=15] > .slick-cell.l18.r21.active').type('{home}');
+    });
+
+    it('should start at Task 5, type "ArrowRight" key 3x times and expect active cell to be at cell 3.3', () => {
+      cy.get('[data-row=15] > .slick-cell.l0.r0').type('{ctrl}{uparrow}');
+      cy.get('[data-row=5] > .slick-cell.l0.r0').contains('Task 5').as('active_cell').click();
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}');
+      cy.get('[data-row=3] > .slick-cell.l3.r7.active').should('have.length', 1);
+    });
+
+    it('should start at Task 5, type "ArrowRight" key 4x times and expect active cell to be at cell 5.8', () => {
+      cy.get('[data-row=5] > .slick-cell.l0.r0').contains('Task 5').as('active_cell').click();
+      cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1);
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}');
+      cy.get('[data-row=5] > .slick-cell.l8.r8.active').should('have.length', 1);
+    });
+
+    it('should start at Task 5, type "ArrowRight" key 4x times, then "ArrowLeft" 4x times and be back at Task 5', () => {
+      cy.get('[data-row=5] > .slick-cell.l0.r0').contains('Task 5').as('active_cell').click();
+      cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1);
+      cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}');
+      cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1);
+    });
+
+    it('should start at Task 2 on Pricing Policy column and type "PageDown" key 3x times and be on Task 59 on same column', () => {
+      cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('@active_cell').type('{pagedown}{pagedown}{pagedown}');
+      cy.get('[data-row=59] > .slick-cell.l2.r2.active').should('have.length', 1);
+    });
+
+    it('should start at Task 59 on Pricing Policy column and type "PageUp" key 3x times and be back to Task 2 on same column', () => {
+      cy.get('[data-row=59] > .slick-cell.l2.r2').as('active_cell').click();
+      cy.get('@active_cell').type('{pageup}{pageup}{pageup}');
+      cy.get('[data-row=2] > .slick-cell.l2.r2.active').should('have.length', 1);
+    });
+
+    it('should start at Task 0 on Policy Index column and type "PageDown" key 2x times but expect active cell to stay on initial cell but still scroll down around Task 40', () => {
+      cy.get('[data-row=0] > .slick-cell.l3.r3').as('active_cell').click();
+      cy.get('@active_cell').type('{pagedown}{pagedown}');
+      cy.get('[data-row=0] > .slick-cell.l3.r3.active').should('have.length', 1);
+      cy.get('[data-row=40]').should('be.visible');
+    });
+
+    it('should start at Task 1 on Excess Cash column and type "PageDown" key 4x times and be on Task 77 on same column', () => {
+      cy.get('[data-row=0] > .slick-cell.l3.r3.active').type('{ctrl}{uparrow}');
+      cy.get('[data-row=1] > .slick-cell.l5.r5').as('active_cell').click();
+      cy.get('@active_cell').type('{pagedown}{pagedown}{pagedown}{pagedown}');
+      cy.get('[data-row=77] > .slick-cell.l5.r5.active').should('have.length', 1);
+    });
+
+    it('should start at Task 77 on Excess Cash column and type "PageDown" key 4x times and be on Task 105 on same column', () => {
+      cy.get('[data-row=77] > .slick-cell.l5.r5').as('active_cell').click();
+      cy.get('@active_cell').type('{pagedown}');
+      cy.get('[data-row=105] > .slick-cell.l5.r5.active').should('have.length', 1);
+    });
+
+    it('should start at Task 105 on Excess Cash column and type "PageUp" key once and be on Task 85 on same column', () => {
+      cy.get('[data-row=105] > .slick-cell.l5.r5').as('active_cell').click();
+      cy.get('@active_cell').type('{pageup}');
+      cy.get('[data-row=85] > .slick-cell.l5.r5.active').should('have.length', 1);
+    });
+
+    it('should start at Task 85 on Excess Cash column and type "PageUp" key once and be on Task 66 on same column', () => {
+      cy.get('[data-row=85] > .slick-cell.l5.r5').as('active_cell').click();
+      cy.get('@active_cell').type('{pageup}');
+      cy.get('[data-row=66] > .slick-cell.l5.r5.active').should('have.length', 1);
+    });
+
+    it('should start at Task 66 on Excess Cash column and type "PageUp" key 3x times and be on Task 9 on same column', () => {
+      cy.get('[data-row=66] > .slick-cell.l5.r5').as('active_cell').click();
+      cy.get('@active_cell').type('{pageup}{pageup}{pageup}');
+      cy.get('[data-row=9] > .slick-cell.l5.r5.active').should('have.length', 1).type('{pageup}');
+    });
+
+    it('should start at Task 0 on Revenue Growth column and type "PageDown" key once and be on Task 88 on same column', () => {
+      cy.get('[data-row=0] > .slick-cell.l1.r1').as('active_cell').click();
+      cy.get('@active_cell').type('{pagedown}');
+      cy.get('[data-row=88] > .slick-cell.l1.r1.active').should('have.length', 1);
+    });
+
+    it('should start at Task 88 on Revenue Growth column and type "PageUp" key once and be on Task 8 on same column', () => {
+      cy.get('[data-row=88] > .slick-cell.l1.r1').as('active_cell').click();
+      cy.get('@active_cell').type('{pageup}');
+      cy.get('[data-row=8] > .slick-cell.l1.r1.active').should('have.length', 1);
+    });
+
+    it('should start at Task 9 on Excess Cash column and type "PageUp" key once and be on Task 0 on same column', () => {
+      cy.get('[data-row=9] > .slick-cell.l5.r5').as('active_cell').click();
+      cy.get('@active_cell').type('{pageup}');
+      cy.get('[data-row=0] > .slick-cell.l5.r5.active').should('have.length', 1);
+    });
+
+    it('should start at Task 9 on Excess Cash column and type "PageDown" key 26x times and be on last Task 499 on same column', () => {
+      cy.get('[data-row=9] > .slick-cell.l5.r5').as('active_cell').click();
+      let command = '';
+      for (let i = 1; i <= 26; i++) {
+        command += '{pagedown}';
+      }
+      cy.get('@active_cell').type(command);
+      cy.get('[data-row=499] > .slick-cell.l5.r5.active').should('have.length', 1);
+    });
+  });
+});
diff --git a/docs/grid-functionalities/column-row-spanning.md b/docs/grid-functionalities/column-row-spanning.md
index 451b09f42..ca5016136 100644
--- a/docs/grid-functionalities/column-row-spanning.md
+++ b/docs/grid-functionalities/column-row-spanning.md
@@ -1,6 +1,12 @@
 ### Description
 You can use Colspan and/or Rowspan by using the DataView Item Metadata Provider, however please note that row spanning is under a flag because of its small perf hit (`rowspan` requires an initial loop through of all row item metadata to map all row span).
 
+> [!NOTE]
+> Please note that `colspan` and `rowspan` have multiple constraints that you must be aware,
+> any side effects will **not** keep anything in sync since metadata are based on grid row index based...
+> for example: Filtering/Sorting/Paging/ColumnReorder/ColumnHidding
+> These side effect will require user's own logic to deal with such things!
+
 ### Demo
 
 #### Colspan / Rowspan
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example33.ts b/examples/vite-demo-vanilla-bundle/src/examples/example33.ts
index 55ebf73ae..c68636356 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example33.ts
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example33.ts
@@ -62,14 +62,6 @@ export default class Example33 {
     },
   };
 
-  get slickerGridInstance() {
-    return this.sgb?.instances;
-  }
-
-  set isFilteringEnabled(enabled: boolean) {
-    this.filteringEnabledClass = enabled ? 'mdi mdi-toggle-switch' : 'mdi mdi-toggle-switch-off-outline';
-  }
-
   constructor() {
     this._bindingEventService = new BindingEventService();
   }
diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md b/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md
index c2026853a..1b9cb80f3 100644
--- a/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md
+++ b/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md
@@ -1,6 +1,12 @@
 ### Description
 You can use Colspan and/or Rowspan by using the DataView Item Metadata Provider, however please note that row spanning is under a flag because of its small perf hit (`rowspan` requires an initial loop through of all row item metadata to map all row span).
 
+> [!NOTE]
+> Please note that `colspan` and `rowspan` have multiple constraints that you must be aware,
+> any side effects will **not** keep anything in sync since metadata are based on grid row index based...
+> for example: Filtering/Sorting/Paging/ColumnReorder/ColumnHidding
+> These side effect will require user's own logic to deal with such things!
+
 ### Demo
 
 #### Colspan / Rowspan
diff --git a/frameworks/slickgrid-vue/src/components/SlickgridVue.vue b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue
index 3c85d3a5b..04b75310f 100644
--- a/frameworks/slickgrid-vue/src/components/SlickgridVue.vue
+++ b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue
@@ -194,7 +194,7 @@ const paginationModel = defineModel<Pagination>('pagination');
 watch(paginationModel, (newPaginationOptions) => paginationOptionsChanged(newPaginationOptions!));
 
 const _columnDefinitions = ref<Column[]>();
-const columnDefinitionsModel = defineModel<Column[]>('columns', { required: true, default: [] });
+const columnDefinitionsModel = defineModel<Column[]>('columns', { required: true, default: [] as Column[] });
 watch(columnDefinitionsModel, (columnDefinitions) => columnDefinitionsChanged(columnDefinitions), { immediate: true });
 
 const dataModel = defineModel<any[]>('data', { required: false }); // technically true but user could use datasetHierarchical instead
@@ -401,7 +401,10 @@ function initialization() {
   }
 
   const dataviewInlineFilters = (_gridOptions.value?.dataView && _gridOptions.value.dataView.inlineFilters) || false;
-  let dataViewOptions: Partial<DataViewOption> = { inlineFilters: dataviewInlineFilters };
+  let dataViewOptions: Partial<DataViewOption> = {
+    ..._gridOptions.value.dataView,
+    inlineFilters: dataviewInlineFilters,
+  } as Partial<DataViewOption>;
 
   if (_gridOptions.value?.draggableGrouping || _gridOptions.value?.enableGrouping) {
     groupItemMetadataProvider = new SlickGroupItemMetadataProvider();
@@ -809,7 +812,7 @@ function bindDifferentHooks(grid: SlickGrid, gridOptions: GridOption, dataView:
         }
       });
 
-      if (gridOptions?.enableFiltering && !gridOptions.enableRowDetailView) {
+      if ((gridOptions?.enableFiltering || gridOptions?.dataView?.globalItemMetadataProvider) && !gridOptions.enableRowDetailView) {
         eventHandler.subscribe(dataView.onRowsChanged, (_e, { calledOnRowCountChanged, rows }) => {
           // filtering data with local dataset will not always show correctly unless we call this updateRow/render
           // also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row
diff --git a/packages/common/src/interfaces/itemMetadata.interface.ts b/packages/common/src/interfaces/itemMetadata.interface.ts
index b6392e6a2..1a1d2311c 100644
--- a/packages/common/src/interfaces/itemMetadata.interface.ts
+++ b/packages/common/src/interfaces/itemMetadata.interface.ts
@@ -10,8 +10,10 @@ export type ColumnMetadata = Pick<
  * and handling of a particular data item. The method should return `null` when the item requires no special handling,
  * or an object following the ItemMetadata interface
  */
+// properties describing metadata related to the item (e.g. grid row) itself
 export interface ItemMetadata {
-  // properties describing metadata related to the item (e.g. grid row) itself
+  /** any attribute types */
+  attributes?: any;
 
   /** One or more (space-separated) CSS classes that will be added to the entire row. */
   cssClasses?: string;
diff --git a/test/cypress/e2e/example32.cy.ts b/test/cypress/e2e/example43.cy.ts
similarity index 100%
rename from test/cypress/e2e/example32.cy.ts
rename to test/cypress/e2e/example43.cy.ts
diff --git a/test/cypress/e2e/example33.cy.ts b/test/cypress/e2e/example44.cy.ts
similarity index 100%
rename from test/cypress/e2e/example33.cy.ts
rename to test/cypress/e2e/example44.cy.ts

From b7fddf0e365583702c073cfb8d395944d739a9c7 Mon Sep 17 00:00:00 2001
From: ghiscoding <gbeaulac@gmail.com>
Date: Sat, 18 Jan 2025 17:46:43 -0500
Subject: [PATCH 2/3] chore: improve styling

---
 demos/vue/src/components/Example43.vue | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/demos/vue/src/components/Example43.vue b/demos/vue/src/components/Example43.vue
index 4b8d1115d..2bc77176d 100644
--- a/demos/vue/src/components/Example43.vue
+++ b/demos/vue/src/components/Example43.vue
@@ -508,10 +508,11 @@ function vueGridReady(grid: SlickgridVueInstance) {
 
   --slick-row-mouse-hover-box-shadow: 0;
   --slick-cell-odd-background-color: #fff;
-  // --slick-cell-border-top: 0;
   --slick-cell-border-right: 1px solid var(--slick-border-color);
-  --slick-cell-border-bottom: 1px solid var(--slick-border-color);
-  // --slick-cell-border-left: 1px;
+  --slick-cell-border-bottom: 0;
+  --slick-cell-border-top: 1px solid var(--slick-border-color);
+  --slick-header-filter-row-border-bottom: 1px solid var(--slick-border-color);
+  --slick-cell-border-left: 0;
   --slick-cell-box-shadow: none;
   --slick-row-mouse-hover-color: #fff;
   --slick-cell-display: flex;

From a7e394db31b98dbefd6dc7567cf14c031ef92215 Mon Sep 17 00:00:00 2001
From: ghiscoding <gbeaulac@gmail.com>
Date: Sat, 18 Jan 2025 17:48:14 -0500
Subject: [PATCH 3/3] chore: fix failing Cypress test

---
 demos/vue/test/cypress/e2e/example17.cy.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/demos/vue/test/cypress/e2e/example17.cy.ts b/demos/vue/test/cypress/e2e/example17.cy.ts
index 619d7842f..54ae19f79 100644
--- a/demos/vue/test/cypress/e2e/example17.cy.ts
+++ b/demos/vue/test/cypress/e2e/example17.cy.ts
@@ -1,10 +1,10 @@
-describe('Example 43 - Dynamically Create Grid from CSV / Excel import', () => {
+describe('Example 17 - Dynamically Create Grid from CSV / Excel import', () => {
   const defaultCsvTitles = ['First Name', 'Last Name', 'Age', 'Type'];
   const GRID_ROW_HEIGHT = 33;
 
   it('should display Example title', () => {
-    cy.visit(`${Cypress.config('baseUrl')}/example43`);
-    cy.get('h2').should('contain', 'Example 43: Dynamically Create Grid from CSV / Excel import');
+    cy.visit(`${Cypress.config('baseUrl')}/example17`);
+    cy.get('h2').should('contain', 'Example 17: Dynamically Create Grid from CSV / Excel import');
   });
 
   it('should load default CSV file and expect default column titles', () => {