diff --git a/samples/grids/grid/keyboard-navigation-guide/.eslintrc.js b/samples/grids/grid/keyboard-navigation-guide/.eslintrc.js
new file mode 100644
index 0000000000..7168b71441
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
\ No newline at end of file
diff --git a/samples/grids/grid/keyboard-navigation-guide/ReadMe.md b/samples/grids/grid/keyboard-navigation-guide/ReadMe.md
new file mode 100644
index 0000000000..ed2f5e5fc6
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/ReadMe.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Keyboard Custom Navigation feature using [Grid](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/grids/grid/keyboard-custom-navigation
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/grids/grid/keyboard-navigation-guide/package.json b/samples/grids/grid/keyboard-navigation-guide/package.json
new file mode 100644
index 0000000000..9246bcd77a
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "example-ignite-ui-react",
+ "description": "This project provides example of using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "set PORT=4200 && react-scripts --max_old_space_size=10240 start",
+ "build": "react-scripts --max_old_space_size=10240 build ",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-dockmanager": "^1.17.0",
+ "igniteui-react": "^19.2.0",
+ "igniteui-react-core": "^19.0.1",
+ "igniteui-react-grids": "^19.2.0",
+ "igniteui-react-inputs": "^19.0.1",
+ "igniteui-react-layouts": "^19.0.1",
+ "igniteui-webcomponents": "^6.2.0",
+ "lit-html": "^3.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "^5.0.1",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^18.11.7",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "react-app-rewired": "^2.2.1",
+ "typescript": "^4.8.4",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/grids/grid/keyboard-navigation-guide/public/index.html b/samples/grids/grid/keyboard-navigation-guide/public/index.html
new file mode 100644
index 0000000000..e2d3265576
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/grids/grid/keyboard-navigation-guide/sandbox.config.json b/samples/grids/grid/keyboard-navigation-guide/sandbox.config.json
new file mode 100644
index 0000000000..07f53508eb
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
\ No newline at end of file
diff --git a/samples/grids/grid/keyboard-navigation-guide/src/NwindData.json b/samples/grids/grid/keyboard-navigation-guide/src/NwindData.json
new file mode 100644
index 0000000000..c00b03ec8d
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/src/NwindData.json
@@ -0,0 +1,458 @@
+[
+ {
+ "ProductID": 1,
+ "ProductName": "Chai",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "10 boxes x 20 bags",
+ "UnitPrice": 18,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2012-02-12",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 2,
+ "ProductName": "Chang",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "24 - 12 oz bottles",
+ "UnitPrice": 19,
+ "UnitsInStock": 17,
+ "UnitsOnOrder": 40,
+ "ReorderLevel": 25,
+ "Discontinued": true,
+ "OrderDate": "2003-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 3,
+ "ProductName": "Aniseed Syrup",
+ "SupplierID": 1,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 550 ml bottles",
+ "UnitPrice": 10,
+ "UnitsInStock": 13,
+ "UnitsOnOrder": 70,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2006-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 4,
+ "ProductName": "Chef Antons Cajun Seasoning",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "48 - 6 oz jars",
+ "UnitPrice": 22,
+ "UnitsInStock": 53,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2016-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 5,
+ "ProductName": "Chef Antons Gumbo Mix",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "36 boxes",
+ "UnitPrice": 21.35,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2011-11-11",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 6,
+ "ProductName": "Grandmas Boysenberry Spread",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 8 oz jars",
+ "UnitPrice": 25,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2017-12-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 7,
+ "ProductName": "Uncle Bobs Organic Dried Pears",
+ "SupplierID": 3,
+ "CategoryID": 7,
+ "QuantityPerUnit": "12 - 1 lb pkgs.",
+ "UnitPrice": 30,
+ "UnitsInStock": 150,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2016-07-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 8,
+ "ProductName": "Northwoods Cranberry Sauce",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 12 oz jars",
+ "UnitPrice": 40,
+ "UnitsInStock": 6,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2018-01-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 9,
+ "ProductName": "Mishi Kobe Niku",
+ "SupplierID": 4,
+ "CategoryID": 6,
+ "QuantityPerUnit": "18 - 500 g pkgs.",
+ "UnitPrice": 97,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2010-02-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 10,
+ "ProductName": "Ikura",
+ "SupplierID": 4,
+ "CategoryID": 8,
+ "QuantityPerUnit": "12 - 200 ml jars",
+ "UnitPrice": 31,
+ "UnitsInStock": 31,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2008-05-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 11,
+ "ProductName": "Queso Cabrales",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "1 kg pkg.",
+ "UnitPrice": 21,
+ "UnitsInStock": 22,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 30,
+ "Discontinued": false,
+ "OrderDate": "2009-01-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 12,
+ "ProductName": "Queso Manchego La Pastora",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "10 - 500 g pkgs.",
+ "UnitPrice": 38,
+ "UnitsInStock": 86,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2015-11-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 13,
+ "ProductName": "Konbu",
+ "SupplierID": 6,
+ "CategoryID": 8,
+ "QuantityPerUnit": "2 kg box",
+ "UnitPrice": 6,
+ "UnitsInStock": 24,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2015-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 14,
+ "ProductName": "Tofu",
+ "SupplierID": 6,
+ "CategoryID": 7,
+ "QuantityPerUnit": "40 - 100 g pkgs.",
+ "UnitPrice": 23.25,
+ "UnitsInStock": 35,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2017-06-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 15,
+ "ProductName": "Genen Shouyu",
+ "SupplierID": 6,
+ "CategoryID": 2,
+ "QuantityPerUnit": "24 - 250 ml bottles",
+ "UnitPrice": 15.5,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2014-03-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ },
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 16,
+ "ProductName": "Pavlova",
+ "SupplierID": 7,
+ "CategoryID": 3,
+ "QuantityPerUnit": "32 - 500 g boxes",
+ "UnitPrice": 17.45,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2018-03-28",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 17,
+ "ProductName": "Alice Mutton",
+ "SupplierID": 7,
+ "CategoryID": 6,
+ "QuantityPerUnit": "20 - 1 kg tins",
+ "UnitPrice": 39,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2015-08-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 18,
+ "ProductName": "Carnarvon Tigers",
+ "SupplierID": 7,
+ "CategoryID": 8,
+ "QuantityPerUnit": "16 kg pkg.",
+ "UnitPrice": 62.5,
+ "UnitsInStock": 42,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-09-27",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ },
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 19,
+ "ProductName": "Teatime Chocolate Biscuits",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "",
+ "UnitPrice": 9.2,
+ "UnitsInStock": 25,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2001-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ }
+ ]
+ },
+ {
+ "ProductID": 20,
+ "ProductName": "Sir Rodneys Marmalade",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "4 - 100 ml jars",
+ "UnitPrice": 4.5,
+ "UnitsInStock": 40,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/samples/grids/grid/keyboard-navigation-guide/src/index.css b/samples/grids/grid/keyboard-navigation-guide/src/index.css
new file mode 100644
index 0000000000..3b330b391d
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/src/index.css
@@ -0,0 +1,7 @@
+/* shared styles are loaded from: */
+/* https://static.infragistics.com/xplatform/css/samples */
+
+ #grid {
+ --ig-size: var(--ig-size-medium);
+ }
+
diff --git a/samples/grids/grid/keyboard-navigation-guide/src/index.tsx b/samples/grids/grid/keyboard-navigation-guide/src/index.tsx
new file mode 100644
index 0000000000..0bad0f60c1
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/src/index.tsx
@@ -0,0 +1,499 @@
+
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import {
+ IgrGrid,
+ IgrGridModule,
+ IgrColumn,
+ IgrColumnGroup,
+ IgrPaginator,
+ IgrGridToolbar,
+ IgrGridMasterDetailContext,
+} from 'igniteui-react-grids';
+import { IgrCheckbox } from 'igniteui-react';
+import { IgrList, IgrListItem, IgrListHeader } from 'igniteui-react';
+import NwindData from './NwindData.json';
+
+import 'igniteui-react-grids/grids/themes/light/bootstrap.css';
+
+IgrGridModule.register();
+
+interface MasterDetailTemplateProps {
+ dataContext: IgrGridMasterDetailContext;
+}
+
+type Customer = {
+ ProductID: number;
+ ProductName: string;
+ SupplierID: number;
+ CategoryID: number;
+ QuantityPerUnit: string;
+ UnitPrice: number;
+ UnitsInStock: number;
+ UnitsOnOrder: number;
+ ReorderLevel: number;
+ Discontinued: boolean;
+ OrderDate: string;
+ Rating: number;
+ Locations?: any[];
+ CompanysAnnualProfit?: number;
+};
+enum GridSection {
+ THEAD = 'thead',
+ TBODY = 'tbody',
+ FOOTER = 'tfoot',
+}
+
+enum ItemAction {
+ Filterable,
+ Sortable,
+ Selectable,
+ Groupable,
+ Collapsible,
+ Expandable,
+ Editable,
+ Always,
+}
+
+type Item = {
+ title: string;
+ subTitle: string;
+ action: ItemAction;
+ active: boolean;
+ completed: boolean;
+};
+
+const makeItem = (
+ title: string,
+ subTitle: string,
+ completed: boolean,
+ action: ItemAction
+): Item => ({ title, subTitle, completed, action, active: action === ItemAction.Always });
+
+const theadKeyCombinations: Item[] = [
+ makeItem('Space', 'select column', false, ItemAction.Selectable),
+ makeItem('Ctrl + ↑/↓', 'sort column asc/desc', false, ItemAction.Sortable),
+ makeItem('Shift + Alt + ←/→', 'group / ungroup column', false, ItemAction.Groupable),
+ makeItem('Alt + arrows', 'expand / collapse column group', false, ItemAction.Collapsible),
+ makeItem('Ctrl + Shift + L', 'open Excel-style filter', false, ItemAction.Filterable),
+ makeItem('Alt + L', 'open Advanced filtering', false, ItemAction.Filterable),
+];
+
+const tbodyKeyCombinations: Item[] = [
+ makeItem('Enter', 'enter edit mode', false, ItemAction.Editable),
+ makeItem('Alt + ←/→', 'expand/collapse group row', false, ItemAction.Expandable),
+ makeItem('Ctrl + Home/End', 'jump to first/last cell', false, ItemAction.Always),
+ makeItem('Alt + →/↓', 'expand row detail', false, ItemAction.Collapsible),
+ makeItem('Alt + ←/↑', 'collapse row detail', false, ItemAction.Collapsible),
+];
+
+const summaryCombinations: Item[] = [
+ makeItem('←', 'move left', false, ItemAction.Always),
+ makeItem('→', 'move right', false, ItemAction.Always),
+ makeItem('Home', 'first summary cell', false, ItemAction.Always),
+ makeItem('End', 'last summary cell', false, ItemAction.Always),
+];
+
+const cloneItems = (items: Item[]) => items.map(i => ({ ...i }));
+
+function enableActions(list: Item[], actions: ItemAction[]) {
+ return list.map(i =>
+ i.action === ItemAction.Always
+ ? { ...i, active: true }
+ : { ...i, active: actions.includes(i.action) }
+ );
+}
+
+function markCompleted(list: Item[], idx: number, value: boolean) {
+ const next = list.slice();
+ next[idx] = { ...next[idx], completed: value };
+ return next;
+}
+
+function withComputed(data: Customer[]): Customer[] {
+ return data.map((item) => {
+ const profit = (100000 + Math.random() * 1000000);
+ return {
+ ...item,
+ CompanysAnnualProfit: profit,
+ };
+ });
+}
+
+const MasterDetailTemplate: React.FC = ({ dataContext }) => {
+ const rowData = dataContext.implicit as Customer;
+
+ return (
+
+
Product Details
+
+
+ Product ID: {rowData.ProductID}
+ Supplier ID: {rowData.SupplierID}
+ Category ID: {rowData.CategoryID}
+ Unit Price: ${rowData.UnitPrice?.toFixed(2)}
+
+
+ Units in Stock: {rowData.UnitsInStock}
+ Units on Order: {rowData.UnitsOnOrder}
+ Reorder Level: {rowData.ReorderLevel}
+ Discontinued: {rowData.Discontinued ? 'Yes' : 'No'}
+
+
+ {rowData.OrderDate && (
+
+ Order Date: {new Date(rowData.OrderDate).toLocaleDateString()}
+
+ )}
+ {rowData.CompanysAnnualProfit && (
+
+ Annual Profit: ${rowData.CompanysAnnualProfit.toLocaleString()}
+
+ )}
+
+ );
+};
+
+export default function GridKeyboardNavGuide() {
+ const gridRef = useRef(null);
+
+ const [data, setData] = useState(() => withComputed(NwindData as Customer[]));
+ const [gridSection, setGridSection] = useState(GridSection.THEAD);
+ const [items, setItems] = useState- (cloneItems(theadKeyCombinations));
+
+ useEffect(() => {
+ const handleGlobalKeyDown = (evt: KeyboardEvent) => {
+ const key = evt.key?.toLowerCase();
+
+ const gridElement = (gridRef.current as any)?.nativeElement;
+ const focusedElement = document.activeElement;
+
+ if (gridElement && (gridElement.contains(focusedElement) || gridElement === focusedElement)) {
+ if (evt.altKey && (key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright')) {
+ if (gridSection === GridSection.TBODY) {
+ if (key === 'arrowup' || key === 'arrowleft') {
+ setTimeout(() => {
+ setItems((prev: Item[]) => markCompleted(prev, 4, true));
+ }, 0);
+ } else if (key === 'arrowdown' || key === 'arrowright') {
+ setTimeout(() => {
+ setItems((prev: Item[]) => markCompleted(prev, 3, true));
+ }, 0);
+ }
+ }
+ }
+
+ if (evt.ctrlKey && (key === 'home' || key === 'end')) {
+ if (gridSection === GridSection.TBODY) {
+ setTimeout(() => {
+ setItems((prev: Item[]) => markCompleted(prev, 2, true));
+ }, 0);
+ }
+ }
+ }
+ };
+
+ document.addEventListener('keydown', handleGlobalKeyDown, true);
+
+ return () => {
+ document.removeEventListener('keydown', handleGlobalKeyDown, true);
+ };
+ }, [gridSection]);
+
+ const headerText = useMemo(() => {
+ if (gridSection === GridSection.THEAD) return 'HEADER COMBINATIONS';
+ if (gridSection === GridSection.TBODY) return 'BODY COMBINATIONS';
+ if (gridSection === GridSection.FOOTER) return 'SUMMARY COMBINATIONS';
+ return '';
+ }, [gridSection]);
+
+ const updateSectionFromActiveNode = () => {
+ const grid = gridRef.current;
+ const active = (grid as any)?.navigation?.activeNode;
+ const rows = grid?.data?.length ?? data.length;
+ if (active && typeof active.row === 'number') {
+ if (active.row < 0) {
+ setGridSection(GridSection.THEAD);
+ setItems(cloneItems(theadKeyCombinations));
+ } else if (active.row >= rows) {
+ setGridSection(GridSection.FOOTER);
+ setItems(cloneItems(summaryCombinations));
+ } else {
+ setGridSection(GridSection.TBODY);
+ setItems(cloneItems(tbodyKeyCombinations));
+ }
+ }
+ };
+
+ const onGridKeyDown = (evt: any) => {
+ const key = evt.key?.toLowerCase();
+ if (!key) return;
+ if (key === 'tab') return;
+
+ if (evt.altKey && (key === 'arrowleft' || key === 'arrowright' || key === 'arrowup' || key === 'arrowdown')) {
+ evt.preventDefault();
+ }
+
+ if ((key === 'l' && evt.altKey) ||
+ (key === 'l' && evt.ctrlKey && evt.shiftKey) ||
+ ((key === 'arrowup' || key === 'arrowdown') && evt.ctrlKey) ||
+ ((key === 'end' || key === 'home') && evt.ctrlKey)) {
+ evt.preventDefault();
+ }
+
+ updateSectionFromActiveNode();
+
+ setItems((prev: Item[]) => {
+ let next = prev.slice();
+
+ if (gridSection === GridSection.FOOTER) {
+ switch (key) {
+ case 'end':
+ next = markCompleted(next, 3, true);
+ break;
+ case 'home':
+ next = markCompleted(next, 2, true);
+ break;
+ case 'arrowleft':
+ next = markCompleted(next, 0, true);
+ break;
+ case 'arrowright':
+ next = markCompleted(next, 1, true);
+ break;
+ }
+ return next;
+ }
+
+ if (gridSection === GridSection.THEAD) {
+ if (key === 'l' && evt.altKey) {
+ next = markCompleted(next, 5, true);
+ return next;
+ }
+ if (key === 'l' && evt.ctrlKey && evt.shiftKey) {
+ next = markCompleted(next, 4, true);
+ }
+ if ((key === 'arrowleft' || key === 'arrowright') && evt.altKey && evt.shiftKey) {
+ next = markCompleted(next, 2, true);
+ }
+ if ((key === 'arrowup' || key === 'arrowdown') && evt.ctrlKey) {
+ next = markCompleted(next, 1, true);
+ }
+ if (key === ' ' || key === 'spacebar') {
+ next = markCompleted(next, 0, true);
+ }
+ }
+
+ if (gridSection === GridSection.TBODY) {
+ if (key === 'enter') {
+ next = markCompleted(next, 0, true);
+ }
+ if ((key === 'end' || key === 'home') && evt.ctrlKey) {
+ next = markCompleted(next, 2, true);
+ }
+ }
+
+ return next;
+ });
+ };
+
+ const refreshHeaderActions = () => {
+ const grid: any = gridRef.current;
+ if (!grid) return;
+
+ if (gridSection !== GridSection.THEAD) return;
+
+ const active = grid?.navigation?.activeNode;
+ const col = grid.visibleColumns?.find(
+ (c: any) => c.visibleIndex === active?.column && c.level === active?.level
+ );
+
+ const actions: ItemAction[] = [];
+ if (col?.sortable) actions.push(ItemAction.Sortable);
+ if (col?.filterable && !col?.columnGroup) actions.push(ItemAction.Filterable);
+ if (col?.collapsible) actions.push(ItemAction.Collapsible);
+ if (col?.groupable) actions.push(ItemAction.Groupable);
+ if (col?.selectable) actions.push(ItemAction.Selectable);
+
+ setItems((prev: Item[]) => enableActions(prev, actions));
+ };
+
+ // When grid focuses a new node, update section + header actions
+ const onActiveNodeChange = () => {
+ updateSectionFromActiveNode();
+ refreshHeaderActions();
+ };
+
+ const onRowToggle = () => {
+ if (gridSection === GridSection.TBODY) {
+ setItems((prev: Item[]) => markCompleted(prev, 1, true));
+ }
+ };
+
+ const onColumnSelectionChanging = (e: any) => {
+ if (e?.detail?.event?.type === 'keydown') {
+ setItems((prev: Item[]) => markCompleted(prev, 0, true));
+ }
+ };
+
+ return (
+
+
+
+
+ {/* Toolbar can be conditionally shown if needed */}
+ {/* */}
+
+ {/* Row detail template removed for simplicity */}
+
+ {/* Column Groups */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Right-side list showing active/available shortcuts */}
+
+
+ {items.length > 0 && (
+
+ {headerText}
+
+ )}
+ {items.map((c: Item, idx: number) => (
+
+
+
+
{c.title}
+
{c.subTitle}
+
+
{
+ const checked = !!e?.detail;
+ setItems((prev: Item[]) => markCompleted(prev, idx, checked));
+ }}
+ />
+
+
+ ))}
+
+ {items.length === 0 && (
+
+
Use the browser navigation until you reach one of the following grid sections:
+
+ - Header
+ - Body
+ - Summary
+
+
When reached, an action list will be shown.
+
+ )}
+
+
+
+ );
+}
+
+// rendering above component in the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/samples/grids/grid/keyboard-navigation-guide/src/react-app-env.d.ts b/samples/grids/grid/keyboard-navigation-guide/src/react-app-env.d.ts
new file mode 100644
index 0000000000..6431bc5fc6
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/samples/grids/grid/keyboard-navigation-guide/tsconfig.json b/samples/grids/grid/keyboard-navigation-guide/tsconfig.json
new file mode 100644
index 0000000000..8c0d146f95
--- /dev/null
+++ b/samples/grids/grid/keyboard-navigation-guide/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}
diff --git a/samples/grids/hierarchical-grid/keyboard-custom-navigation/src/NwindData.json b/samples/grids/hierarchical-grid/keyboard-custom-navigation/src/NwindData.json
new file mode 100644
index 0000000000..c00b03ec8d
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-custom-navigation/src/NwindData.json
@@ -0,0 +1,458 @@
+[
+ {
+ "ProductID": 1,
+ "ProductName": "Chai",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "10 boxes x 20 bags",
+ "UnitPrice": 18,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2012-02-12",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 2,
+ "ProductName": "Chang",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "24 - 12 oz bottles",
+ "UnitPrice": 19,
+ "UnitsInStock": 17,
+ "UnitsOnOrder": 40,
+ "ReorderLevel": 25,
+ "Discontinued": true,
+ "OrderDate": "2003-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 3,
+ "ProductName": "Aniseed Syrup",
+ "SupplierID": 1,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 550 ml bottles",
+ "UnitPrice": 10,
+ "UnitsInStock": 13,
+ "UnitsOnOrder": 70,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2006-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 4,
+ "ProductName": "Chef Antons Cajun Seasoning",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "48 - 6 oz jars",
+ "UnitPrice": 22,
+ "UnitsInStock": 53,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2016-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 5,
+ "ProductName": "Chef Antons Gumbo Mix",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "36 boxes",
+ "UnitPrice": 21.35,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2011-11-11",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 6,
+ "ProductName": "Grandmas Boysenberry Spread",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 8 oz jars",
+ "UnitPrice": 25,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2017-12-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 7,
+ "ProductName": "Uncle Bobs Organic Dried Pears",
+ "SupplierID": 3,
+ "CategoryID": 7,
+ "QuantityPerUnit": "12 - 1 lb pkgs.",
+ "UnitPrice": 30,
+ "UnitsInStock": 150,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2016-07-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 8,
+ "ProductName": "Northwoods Cranberry Sauce",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 12 oz jars",
+ "UnitPrice": 40,
+ "UnitsInStock": 6,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2018-01-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 9,
+ "ProductName": "Mishi Kobe Niku",
+ "SupplierID": 4,
+ "CategoryID": 6,
+ "QuantityPerUnit": "18 - 500 g pkgs.",
+ "UnitPrice": 97,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2010-02-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 10,
+ "ProductName": "Ikura",
+ "SupplierID": 4,
+ "CategoryID": 8,
+ "QuantityPerUnit": "12 - 200 ml jars",
+ "UnitPrice": 31,
+ "UnitsInStock": 31,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2008-05-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 11,
+ "ProductName": "Queso Cabrales",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "1 kg pkg.",
+ "UnitPrice": 21,
+ "UnitsInStock": 22,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 30,
+ "Discontinued": false,
+ "OrderDate": "2009-01-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 12,
+ "ProductName": "Queso Manchego La Pastora",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "10 - 500 g pkgs.",
+ "UnitPrice": 38,
+ "UnitsInStock": 86,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2015-11-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 13,
+ "ProductName": "Konbu",
+ "SupplierID": 6,
+ "CategoryID": 8,
+ "QuantityPerUnit": "2 kg box",
+ "UnitPrice": 6,
+ "UnitsInStock": 24,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2015-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 14,
+ "ProductName": "Tofu",
+ "SupplierID": 6,
+ "CategoryID": 7,
+ "QuantityPerUnit": "40 - 100 g pkgs.",
+ "UnitPrice": 23.25,
+ "UnitsInStock": 35,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2017-06-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 15,
+ "ProductName": "Genen Shouyu",
+ "SupplierID": 6,
+ "CategoryID": 2,
+ "QuantityPerUnit": "24 - 250 ml bottles",
+ "UnitPrice": 15.5,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2014-03-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ },
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 16,
+ "ProductName": "Pavlova",
+ "SupplierID": 7,
+ "CategoryID": 3,
+ "QuantityPerUnit": "32 - 500 g boxes",
+ "UnitPrice": 17.45,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2018-03-28",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 17,
+ "ProductName": "Alice Mutton",
+ "SupplierID": 7,
+ "CategoryID": 6,
+ "QuantityPerUnit": "20 - 1 kg tins",
+ "UnitPrice": 39,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2015-08-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 18,
+ "ProductName": "Carnarvon Tigers",
+ "SupplierID": 7,
+ "CategoryID": 8,
+ "QuantityPerUnit": "16 kg pkg.",
+ "UnitPrice": 62.5,
+ "UnitsInStock": 42,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-09-27",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ },
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 19,
+ "ProductName": "Teatime Chocolate Biscuits",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "",
+ "UnitPrice": 9.2,
+ "UnitsInStock": 25,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2001-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ }
+ ]
+ },
+ {
+ "ProductID": 20,
+ "ProductName": "Sir Rodneys Marmalade",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "4 - 100 ml jars",
+ "UnitPrice": 4.5,
+ "UnitsInStock": 40,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/.eslintrc.js b/samples/grids/hierarchical-grid/keyboard-navigation-guide/.eslintrc.js
new file mode 100644
index 0000000000..7168b71441
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
\ No newline at end of file
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/ReadMe.md b/samples/grids/hierarchical-grid/keyboard-navigation-guide/ReadMe.md
new file mode 100644
index 0000000000..661e102428
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/ReadMe.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Keyboard Custom Navigation feature using [Hierarchical Grid](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/grids/hierarchical-grid/keyboard-custom-navigation
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/package.json b/samples/grids/hierarchical-grid/keyboard-navigation-guide/package.json
new file mode 100644
index 0000000000..9246bcd77a
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "example-ignite-ui-react",
+ "description": "This project provides example of using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "set PORT=4200 && react-scripts --max_old_space_size=10240 start",
+ "build": "react-scripts --max_old_space_size=10240 build ",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-dockmanager": "^1.17.0",
+ "igniteui-react": "^19.2.0",
+ "igniteui-react-core": "^19.0.1",
+ "igniteui-react-grids": "^19.2.0",
+ "igniteui-react-inputs": "^19.0.1",
+ "igniteui-react-layouts": "^19.0.1",
+ "igniteui-webcomponents": "^6.2.0",
+ "lit-html": "^3.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "^5.0.1",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^18.11.7",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "react-app-rewired": "^2.2.1",
+ "typescript": "^4.8.4",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/public/index.html b/samples/grids/hierarchical-grid/keyboard-navigation-guide/public/index.html
new file mode 100644
index 0000000000..e2d3265576
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/sandbox.config.json b/samples/grids/hierarchical-grid/keyboard-navigation-guide/sandbox.config.json
new file mode 100644
index 0000000000..07f53508eb
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
\ No newline at end of file
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/NwindData.json b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/NwindData.json
new file mode 100644
index 0000000000..c00b03ec8d
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/NwindData.json
@@ -0,0 +1,458 @@
+[
+ {
+ "ProductID": 1,
+ "ProductName": "Chai",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "10 boxes x 20 bags",
+ "UnitPrice": 18,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2012-02-12",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 2,
+ "ProductName": "Chang",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "24 - 12 oz bottles",
+ "UnitPrice": 19,
+ "UnitsInStock": 17,
+ "UnitsOnOrder": 40,
+ "ReorderLevel": 25,
+ "Discontinued": true,
+ "OrderDate": "2003-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 3,
+ "ProductName": "Aniseed Syrup",
+ "SupplierID": 1,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 550 ml bottles",
+ "UnitPrice": 10,
+ "UnitsInStock": 13,
+ "UnitsOnOrder": 70,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2006-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 4,
+ "ProductName": "Chef Antons Cajun Seasoning",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "48 - 6 oz jars",
+ "UnitPrice": 22,
+ "UnitsInStock": 53,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2016-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 5,
+ "ProductName": "Chef Antons Gumbo Mix",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "36 boxes",
+ "UnitPrice": 21.35,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2011-11-11",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 6,
+ "ProductName": "Grandmas Boysenberry Spread",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 8 oz jars",
+ "UnitPrice": 25,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2017-12-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 7,
+ "ProductName": "Uncle Bobs Organic Dried Pears",
+ "SupplierID": 3,
+ "CategoryID": 7,
+ "QuantityPerUnit": "12 - 1 lb pkgs.",
+ "UnitPrice": 30,
+ "UnitsInStock": 150,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2016-07-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 8,
+ "ProductName": "Northwoods Cranberry Sauce",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 12 oz jars",
+ "UnitPrice": 40,
+ "UnitsInStock": 6,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2018-01-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 9,
+ "ProductName": "Mishi Kobe Niku",
+ "SupplierID": 4,
+ "CategoryID": 6,
+ "QuantityPerUnit": "18 - 500 g pkgs.",
+ "UnitPrice": 97,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2010-02-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 10,
+ "ProductName": "Ikura",
+ "SupplierID": 4,
+ "CategoryID": 8,
+ "QuantityPerUnit": "12 - 200 ml jars",
+ "UnitPrice": 31,
+ "UnitsInStock": 31,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2008-05-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 11,
+ "ProductName": "Queso Cabrales",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "1 kg pkg.",
+ "UnitPrice": 21,
+ "UnitsInStock": 22,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 30,
+ "Discontinued": false,
+ "OrderDate": "2009-01-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 12,
+ "ProductName": "Queso Manchego La Pastora",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "10 - 500 g pkgs.",
+ "UnitPrice": 38,
+ "UnitsInStock": 86,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2015-11-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 13,
+ "ProductName": "Konbu",
+ "SupplierID": 6,
+ "CategoryID": 8,
+ "QuantityPerUnit": "2 kg box",
+ "UnitPrice": 6,
+ "UnitsInStock": 24,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2015-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 14,
+ "ProductName": "Tofu",
+ "SupplierID": 6,
+ "CategoryID": 7,
+ "QuantityPerUnit": "40 - 100 g pkgs.",
+ "UnitPrice": 23.25,
+ "UnitsInStock": 35,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2017-06-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 15,
+ "ProductName": "Genen Shouyu",
+ "SupplierID": 6,
+ "CategoryID": 2,
+ "QuantityPerUnit": "24 - 250 ml bottles",
+ "UnitPrice": 15.5,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2014-03-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ },
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 16,
+ "ProductName": "Pavlova",
+ "SupplierID": 7,
+ "CategoryID": 3,
+ "QuantityPerUnit": "32 - 500 g boxes",
+ "UnitPrice": 17.45,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2018-03-28",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 17,
+ "ProductName": "Alice Mutton",
+ "SupplierID": 7,
+ "CategoryID": 6,
+ "QuantityPerUnit": "20 - 1 kg tins",
+ "UnitPrice": 39,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2015-08-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 18,
+ "ProductName": "Carnarvon Tigers",
+ "SupplierID": 7,
+ "CategoryID": 8,
+ "QuantityPerUnit": "16 kg pkg.",
+ "UnitPrice": 62.5,
+ "UnitsInStock": 42,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-09-27",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ },
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 19,
+ "ProductName": "Teatime Chocolate Biscuits",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "",
+ "UnitPrice": 9.2,
+ "UnitsInStock": 25,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2001-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ }
+ ]
+ },
+ {
+ "ProductID": 20,
+ "ProductName": "Sir Rodneys Marmalade",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "4 - 100 ml jars",
+ "UnitPrice": 4.5,
+ "UnitsInStock": 40,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/SingersData.json b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/SingersData.json
new file mode 100644
index 0000000000..75ec4ec561
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/SingersData.json
@@ -0,0 +1,2496 @@
+[
+ {
+ "ID": 0,
+ "Artist": "Naomí Yepes",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/naomi.jpg",
+ "Debut": 2011,
+ "GrammyNominations": 6,
+ "GrammyAwards": 0,
+ "HasGrammyAward": false,
+ "Tours": [
+ {
+ "Tour": "Faithful Tour",
+ "StartedOn": "Sep 12",
+ "Location": "Worldwide",
+ "Headliner": "NO",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "City Jam Sessions",
+ "StartedOn": "Aug 13",
+ "Location": "North America",
+ "Headliner": "YES",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "Christmas NYC 2013",
+ "StartedOn": "Dec 13",
+ "Location": "United States",
+ "Headliner": "NO",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "Christmas NYC 2014",
+ "StartedOn": "Dec 14",
+ "Location": "North America",
+ "Headliner": "NO",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "Watermelon Tour",
+ "StartedOn": "Feb 15",
+ "Location": "Worldwide",
+ "Headliner": "YES",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "Christmas NYC 2016",
+ "StartedOn": "Dec 16",
+ "Location": "United States",
+ "Headliner": "NO",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "The Dragon Tour",
+ "StartedOn": "Feb 17",
+ "Location": "Worldwide",
+ "Headliner": "NO",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "Organic Sessions",
+ "StartedOn": "Aug 18",
+ "Location": "United States, England",
+ "Headliner": "YES",
+ "TouredBy": "Naomí Yepes"
+ },
+ {
+ "Tour": "Hope World Tour",
+ "StartedOn": "Mar 19",
+ "Location": "Worldwide",
+ "Headliner": "NO",
+ "TouredBy": "Naomí Yepes"
+ }
+ ],
+ "Albums": [
+ {
+ "Album": "Initiation",
+ "LaunchDate": "September 3, 2013",
+ "BillboardReview": 86,
+ "USBillboard200": 1,
+ "Artist": "Naomí Yepes",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Lonely Falling",
+ "Released": "01 May 2019",
+ "Genre": "*",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 2,
+ "Title": "Bright Breaking",
+ "Released": "25 Jul 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 3,
+ "Title": "River of Whisper",
+ "Released": "29 Jan 2020",
+ "Genre": "Electro house Electropop",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 4,
+ "Title": "Sky of Storm",
+ "Released": "07 May 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 5,
+ "Title": "Electric River",
+ "Released": "20 Jan 2020",
+ "Genre": "R&B",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 6,
+ "Title": "Storm of Storm",
+ "Released": "01 Sep 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 7,
+ "Title": "Fire of Dream",
+ "Released": "12 Sep 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 8,
+ "Title": "Burning in the Light",
+ "Released": "15 Apr 2019",
+ "Genre": "*",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 9,
+ "Title": "Burning in the Storm",
+ "Released": "10 Sep 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Initiation"
+ },
+ {
+ "Number": 10,
+ "Title": "Shadow of Whisper",
+ "Released": "06 Jan 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Initiation"
+ }
+ ]
+ },
+ {
+ "Album": "Dream Driven",
+ "LaunchDate": "August 25, 2014",
+ "BillboardReview": 81,
+ "USBillboard200": 1,
+ "Artist": "Naomí Yepes",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Intro",
+ "Released": "10 Sep 2019",
+ "Genre": "*",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 2,
+ "Title": "Ferocious",
+ "Released": "28 Apr 2014",
+ "Genre": "Dance-pop R&B",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 3,
+ "Title": "Going crazy",
+ "Released": "10 Feb 2015",
+ "Genre": "Dance-pop EDM",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 4,
+ "Title": "Future past",
+ "Released": "20 Sep 2011",
+ "Genre": "*",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 5,
+ "Title": "Roaming like them",
+ "Released": "2 Jul 2014",
+ "Genre": "Electro house Electropop",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 6,
+ "Title": "Last Wishes",
+ "Released": "12 Aug 2014",
+ "Genre": "R&B",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 7,
+ "Title": "Stay where you are",
+ "Released": "14 Sep 2013",
+ "Genre": "*",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 8,
+ "Title": "Imaginarium",
+ "Released": "20 Sep 2021",
+ "Genre": "*",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 9,
+ "Title": "Tell me",
+ "Released": "30 Sep 2014",
+ "Genre": "Synth-pop R&B",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 10,
+ "Title": "Shredded into pieces",
+ "Released": "11 Sep 2015",
+ "Genre": "*",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 11,
+ "Title": "Capture this moment",
+ "Released": "13 Sep 2016",
+ "Genre": "*",
+ "Album": "Dream Driven"
+ },
+ {
+ "Number": 12,
+ "Title": "Dream Driven",
+ "Released": "14 Sep 2014",
+ "Genre": "*",
+ "Album": "Dream Driven"
+ }
+ ]
+ },
+ {
+ "Album": "The dragon journey",
+ "LaunchDate": "May 20, 2016",
+ "BillboardReview": 60,
+ "USBillboard200": 2,
+ "Artist": "Naomí Yepes",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Calling in the Storm",
+ "Released": "12 Mar 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 2,
+ "Title": "Hiding in the Dream",
+ "Released": "23 Mar 2019",
+ "Genre": "R&B",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 3,
+ "Title": "Electric Heart",
+ "Released": "17 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 4,
+ "Title": "Shadow of Echo",
+ "Released": "20 Feb 2019",
+ "Genre": "ethno-tunes",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 5,
+ "Title": "Flying in the Storm",
+ "Released": "08 Apr 2019",
+ "Genre": "R&B",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 6,
+ "Title": "Dark Waiting",
+ "Released": "20 Oct 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 7,
+ "Title": "Fire of River",
+ "Released": "20 Feb 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 8,
+ "Title": "Wild Crying",
+ "Released": "14 Jun 2019",
+ "Genre": "R&B",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 9,
+ "Title": "Bright Dancing",
+ "Released": "14 Mar 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "The dragon journey"
+ },
+ {
+ "Number": 10,
+ "Title": "Golden Waiting",
+ "Released": "12 Sep 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "The dragon journey"
+ }
+ ]
+ },
+ {
+ "Album": "Organic me",
+ "LaunchDate": "August 17, 2018",
+ "BillboardReview": 82,
+ "USBillboard200": 1,
+ "Artist": "Naomí Yepes",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "I Love",
+ "Released": "11 May 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 2,
+ "Title": "Early Morning Compass",
+ "Released": "15 Jan 2020",
+ "Genre": "mystical parody-bap ",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 3,
+ "Title": "Key Fields Forever",
+ "Released": "2 Jan 2020",
+ "Genre": "Dance-pop EDM",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 4,
+ "Title": "Stand by Your Goblins",
+ "Released": "20 Nov 2019",
+ "Genre": "*",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 5,
+ "Title": "Mad to Walk",
+ "Released": "12 May 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 6,
+ "Title": "Alice's Waiting",
+ "Released": "28 Jan 2020",
+ "Genre": "R&B",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 7,
+ "Title": "We Shall Kiss",
+ "Released": "30 Oct 2019",
+ "Genre": "*",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 8,
+ "Title": "Behind Single Ants",
+ "Released": "2 Oct 2019",
+ "Genre": "*",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 9,
+ "Title": "Soap Autopsy",
+ "Released": "8 Aug 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 10,
+ "Title": "Have You Met Rich?",
+ "Released": "1 Jul 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Organic me"
+ },
+ {
+ "Number": 11,
+ "Title": "Livin' on a Banana",
+ "Released": "22 Nov 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Organic me"
+ }
+ ]
+ },
+ {
+ "Album": "Curiosity",
+ "LaunchDate": "December 7, 2019",
+ "BillboardReview": 75,
+ "USBillboard200": 12,
+ "Artist": "Naomí Yepes",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Storm of Dream",
+ "Released": "05 Jan 2019",
+ "Genre": "*",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 2,
+ "Title": "Echo of Dream",
+ "Released": "28 Jan 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 3,
+ "Title": "Light of Shadow",
+ "Released": "07 Feb 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 4,
+ "Title": "Storm of Heart",
+ "Released": "05 Jan 2020",
+ "Genre": "Electro house Electropop",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 5,
+ "Title": "Shadow of River",
+ "Released": "27 Feb 2019",
+ "Genre": "*",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 6,
+ "Title": "Wicked Dancing",
+ "Released": "17 Jan 2020",
+ "Genre": "ethno-tunes",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 7,
+ "Title": "River of Light",
+ "Released": "22 Feb 2019",
+ "Genre": "R&B",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 8,
+ "Title": "Lonely Breaking",
+ "Released": "09 Sep 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 9,
+ "Title": "Furious Flying",
+ "Released": "08 Jun 2019",
+ "Genre": "R&B",
+ "Album": "Curiosity"
+ },
+ {
+ "Number": 10,
+ "Title": "Hiding in the Storm",
+ "Released": "27 May 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Curiosity"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 1,
+ "Artist": "Babila Ebwélé",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/babila.jpg",
+ "Debut": 2009,
+ "GrammyNominations": 0,
+ "GrammyAwards": 11,
+ "HasGrammyAward": true,
+ "Tours": [
+ {
+ "Tour": "The last straw",
+ "StartedOn": "May 09",
+ "Location": "Europe, Asia",
+ "Headliner": "NO",
+ "TouredBy": "Babila Ebwélé"
+ },
+ {
+ "Tour": "No foundations",
+ "StartedOn": "Jun 04",
+ "Location": "United States, Europe",
+ "Headliner": "YES",
+ "TouredBy": "Babila Ebwélé"
+ },
+ {
+ "Tour": "Crazy eyes",
+ "StartedOn": "Jun 08",
+ "Location": "North America",
+ "Headliner": "NO",
+ "TouredBy": "Babila Ebwélé"
+ },
+ {
+ "Tour": "Zero gravity",
+ "StartedOn": "Apr 19",
+ "Location": "United States",
+ "Headliner": "NO",
+ "TouredBy": "Babila Ebwélé"
+ },
+ {
+ "Tour": "Battle with myself",
+ "StartedOn": "Mar 08",
+ "Location": "North America",
+ "Headliner": "YES",
+ "TouredBy": "Babila Ebwélé"
+ }
+ ],
+ "Albums": [
+ {
+ "Album": "Pushing up daisies",
+ "LaunchDate": "May 31, 2000",
+ "BillboardReview": 86,
+ "USBillboard200": 42,
+ "Artist": "Babila Ebwélé",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Wood Shavings Forever",
+ "Released": "9 Jun 2019",
+ "Genre": "*",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 2,
+ "Title": "Early Morning Drive",
+ "Released": "20 May 2019",
+ "Genre": "*",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 3,
+ "Title": "Don't Natter",
+ "Released": "10 Jun 2019",
+ "Genre": "adult calypso-industrial",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 4,
+ "Title": "Stairway to Balloons",
+ "Released": "18 Jun 2019",
+ "Genre": "calypso and mariachi",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 5,
+ "Title": "The Number of your Apple",
+ "Released": "29 Oct 2019",
+ "Genre": "*",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 6,
+ "Title": "Your Delightful Heart",
+ "Released": "24 Feb 2019",
+ "Genre": "*",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 7,
+ "Title": "Nice Weather For Balloons",
+ "Released": "1 Aug 2019",
+ "Genre": "rap-hop",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 8,
+ "Title": "The Girl From Cornwall",
+ "Released": "4 May 2019",
+ "Genre": "enigmatic rock-and-roll",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 9,
+ "Title": "Here Without Jack",
+ "Released": "24 Oct 2019",
+ "Genre": "*",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 10,
+ "Title": "Born Rancid",
+ "Released": "19 Mar 2019",
+ "Genre": "*",
+ "Album": "Pushing up daisies"
+ }
+ ]
+ },
+ {
+ "Album": "Death's dead",
+ "LaunchDate": "June 8, 2016",
+ "BillboardReview": 85,
+ "USBillboard200": 95,
+ "Artist": "Babila Ebwélé",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Men Sound Better With You",
+ "Released": "20 Oct 2019",
+ "Genre": "rap-hop",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 2,
+ "Title": "Ghost in My Rod",
+ "Released": "5 Oct 2019",
+ "Genre": "enigmatic rock-and-roll",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 3,
+ "Title": "Bed of Men",
+ "Released": "14 Nov 2019",
+ "Genre": "whimsical comedy-grass ",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 4,
+ "Title": "Don't Push",
+ "Released": "2 Jan 2020",
+ "Genre": "unblack electronic-trip-hop",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 5,
+ "Title": "Nice Weather For Men",
+ "Released": "18 Dec 2019",
+ "Genre": "*",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 6,
+ "Title": "Rancid Rhapsody",
+ "Released": "10 Mar 2019",
+ "Genre": "*",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 7,
+ "Title": "Push, Push, Push!",
+ "Released": "21 Feb 2019",
+ "Genre": "*",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 8,
+ "Title": "My Name is Sarah",
+ "Released": "15 Nov 2019",
+ "Genre": "*",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 9,
+ "Title": "The Girl From My Hotel",
+ "Released": "6 Nov 2019",
+ "Genre": "*",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 10,
+ "Title": "Free Box",
+ "Released": "18 Apr 2019",
+ "Genre": "splitter-funk",
+ "Album": "Death's dead"
+ },
+ {
+ "Number": 11,
+ "Title": "Hotel Cardiff",
+ "Released": "30 Dec 2019",
+ "Genre": "guilty pleasure ebm",
+ "Album": "Death's dead"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 2,
+ "Artist": "Ahmad Nazeri",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/ahmad.jpg",
+ "Debut": 2004,
+ "GrammyNominations": 3,
+ "GrammyAwards": 1,
+ "HasGrammyAward": true,
+ "Tours": [
+
+ ],
+ "Albums": [
+ {
+ "Album": "Emergency",
+ "LaunchDate": "March 6, 2004",
+ "BillboardReview": 98,
+ "USBillboard200": 69,
+ "Artist": "Ahmad Nazeri",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Gentle Falling",
+ "Released": "26 Apr 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 2,
+ "Title": "Calling in the Fire",
+ "Released": "03 Sep 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 3,
+ "Title": "Fire of Shadow",
+ "Released": "05 Jan 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 4,
+ "Title": "Dancing in the Dream",
+ "Released": "15 Apr 2019",
+ "Genre": "R&B",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 5,
+ "Title": "Calling in the Shadow",
+ "Released": "09 Oct 2019",
+ "Genre": "R&B",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 6,
+ "Title": "Falling in the Sky",
+ "Released": "08 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 7,
+ "Title": "Calling in the Storm",
+ "Released": "05 Dec 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 8,
+ "Title": "Falling in the River",
+ "Released": "19 Aug 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 9,
+ "Title": "Electric Fire",
+ "Released": "30 Nov 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Emergency"
+ },
+ {
+ "Number": 10,
+ "Title": "Lonely River",
+ "Released": "11 Nov 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Emergency"
+ }
+ ]
+ },
+ {
+ "Album": "Bursting bubbles",
+ "LaunchDate": "April 17, 2006",
+ "BillboardReview": 69,
+ "USBillboard200": 39,
+ "Artist": "Ahmad Nazeri",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Lonely Dream",
+ "Released": "11 Dec 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 2,
+ "Title": "Fire of River",
+ "Released": "01 Aug 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 3,
+ "Title": "Wicked Falling",
+ "Released": "25 Jan 2019",
+ "Genre": "*",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 4,
+ "Title": "Crying in the Shadow",
+ "Released": "04 Jan 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 5,
+ "Title": "Wild Burning",
+ "Released": "10 May 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 6,
+ "Title": "Waiting in the Heart",
+ "Released": "07 Aug 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 7,
+ "Title": "Fire of Fire",
+ "Released": "16 May 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 8,
+ "Title": "Bright Heart",
+ "Released": "14 Mar 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 9,
+ "Title": "Lonely Fire",
+ "Released": "15 Oct 2019",
+ "Genre": "R&B",
+ "Album": "Bursting bubbles"
+ },
+ {
+ "Number": 10,
+ "Title": "Sky of Dream",
+ "Released": "20 Jun 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Bursting bubbles"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 3,
+ "Artist": "Kimmy McIlmorie",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/kimmy.jpg",
+ "Debut": 2007,
+ "GrammyNominations": 21,
+ "GrammyAwards": 3,
+ "HasGrammyAward": true,
+ "Tours": [
+
+ ],
+ "Albums": [
+ {
+ "Album": "Here we go again",
+ "LaunchDate": "November 18, 2017",
+ "BillboardReview": 68,
+ "USBillboard200": 1,
+ "Artist": "Kimmy McIlmorie",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Storm of Sky",
+ "Released": "04 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 2,
+ "Title": "Dream of Shadow",
+ "Released": "03 Jan 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 3,
+ "Title": "Dream of Shadow",
+ "Released": "19 Dec 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 4,
+ "Title": "Golden Fire",
+ "Released": "20 Jan 2019",
+ "Genre": "R&B",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 5,
+ "Title": "Running in the Light",
+ "Released": "03 Jan 2020",
+ "Genre": "Synth-pop R&B",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 6,
+ "Title": "Flying in the Heart",
+ "Released": "17 Jan 2019",
+ "Genre": "*",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 7,
+ "Title": "Fire of Storm",
+ "Released": "26 Jan 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 8,
+ "Title": "Calling in the Sky",
+ "Released": "28 Oct 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 9,
+ "Title": "Flying in the Shadow",
+ "Released": "30 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Here we go again"
+ },
+ {
+ "Number": 10,
+ "Title": "Golden Dancing",
+ "Released": "12 Oct 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Here we go again"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 4,
+ "Artist": "Mar Rueda",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/mar.jpg",
+ "Debut": 1996,
+ "GrammyNominations": 14,
+ "GrammyAwards": 2,
+ "HasGrammyAward": true,
+ "Tours": [
+
+ ],
+ "Albums": [
+
+ ]
+ },
+ {
+ "ID": 5,
+ "Artist": "Izabella Tabakova",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/izabella.jpg",
+ "Debut": 2017,
+ "GrammyNominations": 7,
+ "GrammyAwards": 11,
+ "HasGrammyAward": true,
+ "Tours": [
+ {
+ "Tour": "Final breath",
+ "StartedOn": "Jun 13",
+ "Location": "Europe",
+ "Headliner": "YES",
+ "TouredBy": "Izabella Tabakova"
+ },
+ {
+ "Tour": "Once bitten",
+ "StartedOn": "Dec 18",
+ "Location": "Australia, United States",
+ "Headliner": "NO",
+ "TouredBy": "Izabella Tabakova"
+ },
+ {
+ "Tour": "Code word",
+ "StartedOn": "Sep 19",
+ "Location": "United States, Europe",
+ "Headliner": "NO",
+ "TouredBy": "Izabella Tabakova"
+ },
+ {
+ "Tour": "Final draft",
+ "StartedOn": "Sep 17",
+ "Location": "United States, Europe",
+ "Headliner": "YES",
+ "TouredBy": "Izabella Tabakova"
+ }
+ ],
+ "Albums": [
+ {
+ "Album": "Once bitten",
+ "LaunchDate": "July 16, 2007",
+ "BillboardReview": 79,
+ "USBillboard200": 53,
+ "Artist": "Izabella Tabakova",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Whole Lotta Super Cats",
+ "Released": "21 May 2019",
+ "Genre": "*",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 2,
+ "Title": "Enter Becky",
+ "Released": "16 Jan 2020",
+ "Genre": "*",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 3,
+ "Title": "Your Cheatin' Flamingo",
+ "Released": "14 Jan 2020",
+ "Genre": "*",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 4,
+ "Title": "Mad to Kiss",
+ "Released": "6 Nov 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 5,
+ "Title": "Hotel Prague",
+ "Released": "20 Oct 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 6,
+ "Title": "Jail on My Mind",
+ "Released": "31 May 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 7,
+ "Title": "Amazing Blues",
+ "Released": "29 May 2019",
+ "Genre": "mystical parody-bap ",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 8,
+ "Title": "Goody Two Iron Filings",
+ "Released": "4 Jul 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 9,
+ "Title": "I Love in Your Arms",
+ "Released": "7 Jun 2019",
+ "Genre": "R&B",
+ "Album": "Once bitten"
+ },
+ {
+ "Number": 10,
+ "Title": "Truly Madly Amazing",
+ "Released": "12 Sep 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Once bitten"
+ }
+ ]
+ },
+ {
+ "Album": "Your graciousness",
+ "LaunchDate": "November 17, 2004",
+ "BillboardReview": 69,
+ "USBillboard200": 30,
+ "Artist": "Izabella Tabakova",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "We Shall Tickle",
+ "Released": "31 Aug 2019",
+ "Genre": "old emo-garage ",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 2,
+ "Title": "Snail Boogie",
+ "Released": "14 Jun 2019",
+ "Genre": "*",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 3,
+ "Title": "Amazing Liz",
+ "Released": "15 Oct 2019",
+ "Genre": "*",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 4,
+ "Title": "When Sexy Aardvarks Cry",
+ "Released": "1 Oct 2019",
+ "Genre": "whimsical comedy-grass ",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 5,
+ "Title": "Stand By Dave",
+ "Released": "18 Aug 2019",
+ "Genre": "unblack electronic-trip-hop",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 6,
+ "Title": "The Golf Course is Your Land",
+ "Released": "2 Apr 2019",
+ "Genre": "*",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 7,
+ "Title": "Where Have All the Men Gone?",
+ "Released": "29 Apr 2019",
+ "Genre": "*",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 8,
+ "Title": "Rhythm of the Leg",
+ "Released": "5 Aug 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 9,
+ "Title": "Baby, I Need Your Hats",
+ "Released": "5 Dec 2019",
+ "Genre": "neuro-tunes",
+ "Album": "Your graciousness"
+ },
+ {
+ "Number": 10,
+ "Title": "Stand by Your Cat",
+ "Released": "25 Jul 2019",
+ "Genre": "*",
+ "Album": "Your graciousness"
+ }
+ ]
+ },
+ {
+ "Album": "Dark matters",
+ "LaunchDate": "November 3, 2002",
+ "BillboardReview": 79,
+ "USBillboard200": 85,
+ "Artist": "Izabella Tabakova",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Hiding in the Light",
+ "Released": "24 Jan 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 2,
+ "Title": "Furious River",
+ "Released": "13 Jan 2020",
+ "Genre": "Electro house Electropop",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 3,
+ "Title": "Wild Crying",
+ "Released": "27 Feb 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 4,
+ "Title": "Light of Dream",
+ "Released": "01 Jun 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 5,
+ "Title": "Light of Dream",
+ "Released": "24 Aug 2019",
+ "Genre": "*",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 6,
+ "Title": "Storm of Light",
+ "Released": "26 Feb 2019",
+ "Genre": "*",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 7,
+ "Title": "Dark Storm",
+ "Released": "18 Jan 2020",
+ "Genre": "R&B",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 8,
+ "Title": "Dark Calling",
+ "Released": "20 Mar 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 9,
+ "Title": "Sky of Whisper",
+ "Released": "30 Jan 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Dark matters"
+ },
+ {
+ "Number": 10,
+ "Title": "Dancing in the Light",
+ "Released": "28 Nov 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Dark matters"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 6,
+ "Artist": "Nguyễn Diệp Chi",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/nguyen.jpg",
+ "Debut": 1992,
+ "GrammyNominations": 4,
+ "GrammyAwards": 2,
+ "HasGrammyAward": true,
+ "Tours": [
+
+ ],
+ "Albums": [
+ {
+ "Album": "Library of liberty",
+ "LaunchDate": "December 22, 2003",
+ "BillboardReview": 93,
+ "USBillboard200": 5,
+ "Artist": "Nguyễn Diệp Chi",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Echo of River",
+ "Released": "05 Mar 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 2,
+ "Title": "Heart of River",
+ "Released": "12 Jan 2020",
+ "Genre": "Electro house Electropop",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 3,
+ "Title": "Dark Light",
+ "Released": "09 Aug 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 4,
+ "Title": "Dark Fire",
+ "Released": "22 Jun 2019",
+ "Genre": "R&B",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 5,
+ "Title": "Flying in the Fire",
+ "Released": "22 Jul 2019",
+ "Genre": "*",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 6,
+ "Title": "Shadow of Heart",
+ "Released": "02 Jan 2020",
+ "Genre": "*",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 7,
+ "Title": "Fire of Fire",
+ "Released": "27 Jan 2019",
+ "Genre": "*",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 8,
+ "Title": "Falling in the River",
+ "Released": "05 Aug 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 9,
+ "Title": "Fire of Light",
+ "Released": "31 Dec 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Library of liberty"
+ },
+ {
+ "Number": 10,
+ "Title": "Bright Flying",
+ "Released": "24 Jan 2019",
+ "Genre": "*",
+ "Album": "Library of liberty"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 7,
+ "Artist": "Eva Lee",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/eva.jpg",
+ "Debut": 2008,
+ "GrammyNominations": 2,
+ "GrammyAwards": 0,
+ "HasGrammyAward": false,
+ "Tours": [
+
+ ],
+ "Albums": [
+ {
+ "Album": "Just a tease",
+ "LaunchDate": "May 3, 2001",
+ "BillboardReview": 91,
+ "USBillboard200": 29,
+ "Artist": "Eva Lee",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Dancing in the Shadow",
+ "Released": "02 Aug 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 2,
+ "Title": "Silent Whisper",
+ "Released": "09 Jul 2019",
+ "Genre": "*",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 3,
+ "Title": "Crying in the Whisper",
+ "Released": "30 May 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 4,
+ "Title": "River of Light",
+ "Released": "10 Jan 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 5,
+ "Title": "Golden River",
+ "Released": "15 Nov 2019",
+ "Genre": "*",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 6,
+ "Title": "Burning in the Shadow",
+ "Released": "18 Apr 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 7,
+ "Title": "Shadow of Sky",
+ "Released": "06 Sep 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 8,
+ "Title": "Gentle Waiting",
+ "Released": "05 Dec 2019",
+ "Genre": "R&B",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 9,
+ "Title": "Bright River",
+ "Released": "27 Jan 2020",
+ "Genre": "R&B",
+ "Album": "Just a tease"
+ },
+ {
+ "Number": 10,
+ "Title": "Heart of Storm",
+ "Released": "07 Jan 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Just a tease"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 8,
+ "Artist": "Siri Jakobsson",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/siri.jpg",
+ "Debut": 1990,
+ "GrammyNominations": 2,
+ "GrammyAwards": 8,
+ "HasGrammyAward": true,
+ "Tours": [
+ {
+ "Tour": "Basket case",
+ "StartedOn": "Jan 07",
+ "Location": "Europe, Asia",
+ "Headliner": "NO",
+ "TouredBy": "Siri Jakobsson"
+ },
+ {
+ "Tour": "The bigger fish",
+ "StartedOn": "Dec 07",
+ "Location": "United States, Europe",
+ "Headliner": "YES",
+ "TouredBy": "Siri Jakobsson"
+ },
+ {
+ "Tour": "Missed the boat",
+ "StartedOn": "Jun 09",
+ "Location": "Europe, Asia",
+ "Headliner": "NO",
+ "TouredBy": "Siri Jakobsson"
+ },
+ {
+ "Tour": "Equivalent exchange",
+ "StartedOn": "Feb 06",
+ "Location": "United States, Europe",
+ "Headliner": "YES",
+ "TouredBy": "Siri Jakobsson"
+ },
+ {
+ "Tour": "Damage control",
+ "StartedOn": "Oct 11",
+ "Location": "Australia, United States",
+ "Headliner": "NO",
+ "TouredBy": "Siri Jakobsson"
+ }
+ ],
+ "Albums": [
+ {
+ "Album": "Under the bus",
+ "LaunchDate": "May 14, 2000",
+ "BillboardReview": 67,
+ "USBillboard200": 67,
+ "Artist": "Siri Jakobsson",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Jack Broke My Heart At Tesco's",
+ "Released": "19 Jan 2020",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 2,
+ "Title": "Cat Deep, Hats High",
+ "Released": "5 Dec 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 3,
+ "Title": "In Snail We Trust",
+ "Released": "31 May 2019",
+ "Genre": "hardcore opera",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 4,
+ "Title": "Liz's Waiting",
+ "Released": "22 Jul 2019",
+ "Genre": "emotional C-jam ",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 5,
+ "Title": "Lifeless Blues",
+ "Released": "14 Jun 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 6,
+ "Title": "I Spin",
+ "Released": "26 Mar 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 7,
+ "Title": "Ring of Rock",
+ "Released": "12 Dec 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 8,
+ "Title": "Livin' on a Rock",
+ "Released": "17 Apr 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 9,
+ "Title": "Your Lifeless Heart",
+ "Released": "15 Sep 2019",
+ "Genre": "adult calypso-industrial",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 10,
+ "Title": "The High Street on My Mind",
+ "Released": "11 Nov 2019",
+ "Genre": "calypso and mariachi",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 11,
+ "Title": "Behind Ugly Curtains",
+ "Released": "8 May 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 12,
+ "Title": "Where Have All the Curtains Gone?",
+ "Released": "28 Jun 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 13,
+ "Title": "Ghost in My Apple",
+ "Released": "14 Dec 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ },
+ {
+ "Number": 14,
+ "Title": "I Chatter",
+ "Released": "30 Nov 2019",
+ "Genre": "*",
+ "Album": "Under the bus"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 9,
+ "Artist": "Pablo Cambeiro",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/pablo.jpg",
+ "Debut": 2011,
+ "GrammyNominations": 5,
+ "GrammyAwards": 0,
+ "HasGrammyAward": false,
+ "Tours": [
+ {
+ "Tour": "Beads",
+ "StartedOn": "May 11",
+ "Location": "Worldwide",
+ "Headliner": "NO",
+ "TouredBy": "Pablo Cambeiro"
+ },
+ {
+ "Tour": "Concept art",
+ "StartedOn": "Dec 18",
+ "Location": "United States",
+ "Headliner": "YES",
+ "TouredBy": "Pablo Cambeiro"
+ },
+ {
+ "Tour": "Glass shoe",
+ "StartedOn": "Jan 20",
+ "Location": "Worldwide",
+ "Headliner": "YES",
+ "TouredBy": "Pablo Cambeiro"
+ },
+ {
+ "Tour": "Pushing buttons",
+ "StartedOn": "Feb 15",
+ "Location": "Europe, Asia",
+ "Headliner": "NO",
+ "TouredBy": "Pablo Cambeiro"
+ },
+ {
+ "Tour": "Dark matters",
+ "StartedOn": "Jan 04",
+ "Location": "Australia, United States",
+ "Headliner": "YES",
+ "TouredBy": "Pablo Cambeiro"
+ },
+ {
+ "Tour": "Greener grass",
+ "StartedOn": "Sep 09",
+ "Location": "United States, Europe",
+ "Headliner": "NO",
+ "TouredBy": "Pablo Cambeiro"
+ },
+ {
+ "Tour": "Apparatus",
+ "StartedOn": "Nov 16",
+ "Location": "Europe",
+ "Headliner": "NO",
+ "TouredBy": "Pablo Cambeiro"
+ }
+ ],
+ "Albums": [
+ {
+ "Album": "Fluke",
+ "LaunchDate": "August 4, 2017",
+ "BillboardReview": 93,
+ "USBillboard200": 98,
+ "Artist": "Pablo Cambeiro",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Dancing in the Echo",
+ "Released": "03 Oct 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 2,
+ "Title": "Dream of Dream",
+ "Released": "03 Mar 2019",
+ "Genre": "*",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 3,
+ "Title": "Calling in the Echo",
+ "Released": "16 Sep 2019",
+ "Genre": "*",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 4,
+ "Title": "Light of Light",
+ "Released": "25 May 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 5,
+ "Title": "Bright Light",
+ "Released": "21 Mar 2019",
+ "Genre": "R&B",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 6,
+ "Title": "Storm of Echo",
+ "Released": "17 Jul 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 7,
+ "Title": "Lonely Calling",
+ "Released": "10 Apr 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 8,
+ "Title": "Gentle Falling",
+ "Released": "28 Nov 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 9,
+ "Title": "Wild Flying",
+ "Released": "26 Nov 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Fluke"
+ },
+ {
+ "Number": 10,
+ "Title": "Sky of Dream",
+ "Released": "29 May 2019",
+ "Genre": "R&B",
+ "Album": "Fluke"
+ }
+ ]
+ },
+ {
+ "Album": "Crowd control",
+ "LaunchDate": "August 26, 2003",
+ "BillboardReview": 68,
+ "USBillboard200": 84,
+ "Artist": "Pablo Cambeiro",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "My Bed on My Mind",
+ "Released": "25 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 2,
+ "Title": "Bright Blues",
+ "Released": "28 Sep 2019",
+ "Genre": "neuro-tunes",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 3,
+ "Title": "Sail, Sail, Sail!",
+ "Released": "5 Mar 2019",
+ "Genre": "*",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 4,
+ "Title": "Hotel My Bed",
+ "Released": "22 Mar 2019",
+ "Genre": "*",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 5,
+ "Title": "Gonna Make You Mash",
+ "Released": "18 May 2019",
+ "Genre": "*",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 6,
+ "Title": "Straight Outta America",
+ "Released": "16 Jan 2020",
+ "Genre": "hardcore opera",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 7,
+ "Title": "I Drive",
+ "Released": "23 Feb 2019",
+ "Genre": "emotional C-jam ",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 8,
+ "Title": "Like a Teddy",
+ "Released": "31 Aug 2019",
+ "Genre": "*",
+ "Album": "Crowd control"
+ },
+ {
+ "Number": 9,
+ "Title": "Teddy Boogie",
+ "Released": "30 Nov 2019",
+ "Genre": "*",
+ "Album": "Crowd control"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 10,
+ "Artist": "Athar Malakooti",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/athar.jpg",
+ "Debut": 2017,
+ "GrammyNominations": 0,
+ "GrammyAwards": 0,
+ "HasGrammyAward": false,
+ "Tours": [
+
+ ],
+ "Albums": [
+ {
+ "Album": "Pushing up daisies",
+ "LaunchDate": "February 24, 2016",
+ "BillboardReview": 74,
+ "USBillboard200": 77,
+ "Artist": "Athar Malakooti",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Hiding in the Whisper",
+ "Released": "03 Apr 2019",
+ "Genre": "R&B",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 2,
+ "Title": "Wicked Light",
+ "Released": "21 Aug 2019",
+ "Genre": "R&B",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 3,
+ "Title": "Flying in the River",
+ "Released": "03 Feb 2020",
+ "Genre": "Synth-pop R&B",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 4,
+ "Title": "Wicked Hiding",
+ "Released": "15 Sep 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 5,
+ "Title": "Lonely Light",
+ "Released": "13 May 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 6,
+ "Title": "Bright Dancing",
+ "Released": "10 Apr 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 7,
+ "Title": "Gentle Dream",
+ "Released": "21 May 2019",
+ "Genre": "*",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 8,
+ "Title": "Sky of Echo",
+ "Released": "09 Jun 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 9,
+ "Title": "Breaking in the Sky",
+ "Released": "27 Dec 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Pushing up daisies"
+ },
+ {
+ "Number": 10,
+ "Title": "Whisper of Shadow",
+ "Released": "04 Jan 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Pushing up daisies"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 11,
+ "Artist": "Marti Valencia",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/marti.jpg",
+ "Debut": 2004,
+ "GrammyNominations": 1,
+ "GrammyAwards": 1,
+ "HasGrammyAward": true,
+ "Tours": [
+ {
+ "Tour": "Cat eat cat world",
+ "StartedOn": "Sep 00",
+ "Location": "Worldwide",
+ "Headliner": "YES",
+ "TouredBy": "Marti Valencia"
+ },
+ {
+ "Tour": "Final straw",
+ "StartedOn": "Sep 06",
+ "Location": "United States, Europe",
+ "Headliner": "NO",
+ "TouredBy": "Marti Valencia"
+ }
+ ],
+ "Albums": [
+ {
+ "Album": "Nemesis",
+ "LaunchDate": "June 30, 2004",
+ "BillboardReview": 94,
+ "USBillboard200": 9,
+ "Artist": "Marti Valencia",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Hiding in the Sky",
+ "Released": "26 Nov 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 2,
+ "Title": "Waiting in the Echo",
+ "Released": "10 Jul 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 3,
+ "Title": "Wicked Shadow",
+ "Released": "29 Jul 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 4,
+ "Title": "Crying in the Whisper",
+ "Released": "09 Apr 2019",
+ "Genre": "*",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 5,
+ "Title": "Echo of Storm",
+ "Released": "19 Nov 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 6,
+ "Title": "Shadow of Sky",
+ "Released": "24 Jul 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 7,
+ "Title": "Golden Hiding",
+ "Released": "12 Dec 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 8,
+ "Title": "Wild Dancing",
+ "Released": "17 Aug 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 9,
+ "Title": "Bright Burning",
+ "Released": "30 Aug 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Nemesis"
+ },
+ {
+ "Number": 10,
+ "Title": "Flying in the River",
+ "Released": "02 Sep 2019",
+ "Genre": "*",
+ "Album": "Nemesis"
+ }
+ ]
+ },
+ {
+ "Album": "First chance",
+ "LaunchDate": "January 7, 2019",
+ "BillboardReview": 96,
+ "USBillboard200": 19,
+ "Artist": "Marti Valencia",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "My Name is Jason",
+ "Released": "12 Jul 2019",
+ "Genre": "*",
+ "Album": "First chance"
+ },
+ {
+ "Number": 2,
+ "Title": "Amazing Andy",
+ "Released": "5 Mar 2019",
+ "Genre": "*",
+ "Album": "First chance"
+ },
+ {
+ "Number": 3,
+ "Title": "The Number of your Knight",
+ "Released": "4 Dec 2019",
+ "Genre": "*",
+ "Album": "First chance"
+ },
+ {
+ "Number": 4,
+ "Title": "I Sail",
+ "Released": "3 Mar 2019",
+ "Genre": "*",
+ "Album": "First chance"
+ },
+ {
+ "Number": 5,
+ "Title": "Goody Two Hands",
+ "Released": "11 Oct 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "First chance"
+ },
+ {
+ "Number": 6,
+ "Title": "Careful With That Knife",
+ "Released": "18 Dec 2019",
+ "Genre": "R&B",
+ "Album": "First chance"
+ },
+ {
+ "Number": 7,
+ "Title": "Four Single Ants",
+ "Released": "18 Jan 2020",
+ "Genre": "*",
+ "Album": "First chance"
+ },
+ {
+ "Number": 8,
+ "Title": "Kiss Forever",
+ "Released": "10 Aug 2019",
+ "Genre": "*",
+ "Album": "First chance"
+ },
+ {
+ "Number": 9,
+ "Title": "Rich's Waiting",
+ "Released": "15 Mar 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "First chance"
+ },
+ {
+ "Number": 10,
+ "Title": "Japan is Your Land",
+ "Released": "7 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "First chance"
+ },
+ {
+ "Number": 11,
+ "Title": "Pencils in My Banana",
+ "Released": "21 Jun 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "First chance"
+ },
+ {
+ "Number": 12,
+ "Title": "I Sail in Your Arms",
+ "Released": "30 Apr 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "First chance"
+ }
+ ]
+ },
+ {
+ "Album": "God's advocate",
+ "LaunchDate": "April 29, 2007",
+ "BillboardReview": 66,
+ "USBillboard200": 37,
+ "Artist": "Marti Valencia",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Wild River",
+ "Released": "11 Jan 2019",
+ "Genre": "*",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 2,
+ "Title": "Wicked Whisper",
+ "Released": "16 Feb 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 3,
+ "Title": "Storm of Heart",
+ "Released": "11 Aug 2019",
+ "Genre": "*",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 4,
+ "Title": "Golden Dancing",
+ "Released": "02 Mar 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 5,
+ "Title": "Calling in the Sky",
+ "Released": "10 Sep 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 6,
+ "Title": "Calling in the Heart",
+ "Released": "12 Jan 2019",
+ "Genre": "ethno-tunes",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 7,
+ "Title": "Running in the Storm",
+ "Released": "10 Nov 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 8,
+ "Title": "Wild Sky",
+ "Released": "10 Apr 2019",
+ "Genre": "R&B",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 9,
+ "Title": "Crying in the Shadow",
+ "Released": "02 Mar 2019",
+ "Genre": "R&B",
+ "Album": "God's advocate"
+ },
+ {
+ "Number": 10,
+ "Title": "Whisper of River",
+ "Released": "12 May 2019",
+ "Genre": "*",
+ "Album": "God's advocate"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 12,
+ "Artist": "Alicia Stanger",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/alicia.jpg",
+ "Debut": 2010,
+ "GrammyNominations": 1,
+ "GrammyAwards": 0,
+ "HasGrammyAward": false,
+ "Tours": [
+
+ ],
+ "Albums": [
+ {
+ "Album": "Forever alone",
+ "LaunchDate": "November 3, 2005",
+ "BillboardReview": 82,
+ "USBillboard200": 7,
+ "Artist": "Alicia Stanger",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Shadow of Light",
+ "Released": "24 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 2,
+ "Title": "Running in the Echo",
+ "Released": "03 May 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 3,
+ "Title": "Gentle Dream",
+ "Released": "24 Aug 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 4,
+ "Title": "Furious River",
+ "Released": "24 Apr 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 5,
+ "Title": "Wild Whisper",
+ "Released": "09 Mar 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 6,
+ "Title": "Whisper of Sky",
+ "Released": "24 Jul 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 7,
+ "Title": "Lonely Storm",
+ "Released": "01 May 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 8,
+ "Title": "Dancing in the River",
+ "Released": "17 Dec 2019",
+ "Genre": "*",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 9,
+ "Title": "Electric Fire",
+ "Released": "17 Oct 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Forever alone"
+ },
+ {
+ "Number": 10,
+ "Title": "Electric Sky",
+ "Released": "25 Sep 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Forever alone"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "ID": 13,
+ "Artist": "Peter Taylor",
+ "Photo": "https://static.infragistics.com/xplatform/images/people/names/peter.jpg",
+ "Debut": 2005,
+ "GrammyNominations": 0,
+ "GrammyAwards": 2,
+ "HasGrammyAward": true,
+ "Tours": [
+ {
+ "Tour": "Love",
+ "StartedOn": "Jun 04",
+ "Location": "Europe, Asia",
+ "Headliner": "YES",
+ "TouredBy": "Peter Taylor"
+ },
+ {
+ "Tour": "Fault of treasures",
+ "StartedOn": "Oct 13",
+ "Location": "North America",
+ "Headliner": "NO",
+ "TouredBy": "Peter Taylor"
+ },
+ {
+ "Tour": "For eternity",
+ "StartedOn": "Mar 05",
+ "Location": "United States",
+ "Headliner": "YES",
+ "TouredBy": "Peter Taylor"
+ },
+ {
+ "Tour": "Time flies",
+ "StartedOn": "Jun 03",
+ "Location": "North America",
+ "Headliner": "NO",
+ "TouredBy": "Peter Taylor"
+ },
+ {
+ "Tour": "Highest difficulty",
+ "StartedOn": "Nov 01",
+ "Location": "Worldwide",
+ "Headliner": "YES",
+ "TouredBy": "Peter Taylor"
+ },
+ {
+ "Tour": "Sleeping dogs",
+ "StartedOn": "May 04",
+ "Location": "United States, Europe",
+ "Headliner": "NO",
+ "TouredBy": "Peter Taylor"
+ }
+ ],
+ "Albums": [
+ {
+ "Album": "Decisions decisions",
+ "LaunchDate": "April 10, 2008",
+ "BillboardReview": 85,
+ "USBillboard200": 35,
+ "Artist": "Peter Taylor",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Calling in the Dream",
+ "Released": "01 Aug 2019",
+ "Genre": "R&B",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 2,
+ "Title": "Electric Burning",
+ "Released": "10 Sep 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 3,
+ "Title": "Dark Flying",
+ "Released": "28 Apr 2019",
+ "Genre": "*",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 4,
+ "Title": "Gentle Sky",
+ "Released": "20 Nov 2019",
+ "Genre": "ethno-tunes",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 5,
+ "Title": "Gentle Calling",
+ "Released": "13 Jan 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 6,
+ "Title": "Golden Falling",
+ "Released": "14 Feb 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 7,
+ "Title": "Silent River",
+ "Released": "13 Feb 2019",
+ "Genre": "R&B",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 8,
+ "Title": "Furious Calling",
+ "Released": "11 Jun 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 9,
+ "Title": "Running in the Echo",
+ "Released": "06 Nov 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Decisions decisions"
+ },
+ {
+ "Number": 10,
+ "Title": "Furious River",
+ "Released": "12 Mar 2019",
+ "Genre": "*",
+ "Album": "Decisions decisions"
+ }
+ ]
+ },
+ {
+ "Album": "Climate changed",
+ "LaunchDate": "June 20, 2015",
+ "BillboardReview": 66,
+ "USBillboard200": 89,
+ "Artist": "Peter Taylor",
+ "Songs": [
+ {
+ "Number": 1,
+ "Title": "Dark Crying",
+ "Released": "27 Apr 2019",
+ "Genre": "Electro house Electropop",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 2,
+ "Title": "Dark Waiting",
+ "Released": "14 Nov 2019",
+ "Genre": "Synth-pop R&B",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 3,
+ "Title": "Furious Waiting",
+ "Released": "23 May 2019",
+ "Genre": "*",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 4,
+ "Title": "Running in the Echo",
+ "Released": "29 Nov 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 5,
+ "Title": "Dream of Sky",
+ "Released": "31 Oct 2019",
+ "Genre": "Crunk reggaeton",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 6,
+ "Title": "Hiding in the Heart",
+ "Released": "09 Aug 2019",
+ "Genre": "R&B",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 7,
+ "Title": "Sky of Storm",
+ "Released": "01 Jun 2019",
+ "Genre": "R&B",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 8,
+ "Title": "Light of Storm",
+ "Released": "17 Jan 2020",
+ "Genre": "ethno-tunes",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 9,
+ "Title": "Light of Sky",
+ "Released": "26 May 2019",
+ "Genre": "*",
+ "Album": "Climate changed"
+ },
+ {
+ "Number": 10,
+ "Title": "Golden River",
+ "Released": "19 Jun 2019",
+ "Genre": "*",
+ "Album": "Climate changed"
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/index.css b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/index.css
new file mode 100644
index 0000000000..3b330b391d
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/index.css
@@ -0,0 +1,7 @@
+/* shared styles are loaded from: */
+/* https://static.infragistics.com/xplatform/css/samples */
+
+ #grid {
+ --ig-size: var(--ig-size-medium);
+ }
+
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/index.tsx b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/index.tsx
new file mode 100644
index 0000000000..dd3a70277b
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/index.tsx
@@ -0,0 +1,305 @@
+
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import {
+ IgrHierarchicalGrid,
+ IgrHierarchicalGridModule,
+ IgrColumn,
+ IgrRowIsland,
+ IgrPaginator,
+} from 'igniteui-react-grids';
+import { IgrCheckbox } from 'igniteui-react';
+import { IgrList, IgrListItem, IgrListHeader } from 'igniteui-react';
+import SingersData from './SingersData.json';
+
+import 'igniteui-react-grids/grids/themes/light/bootstrap.css';
+
+type Singer = {
+ ID: number;
+ Artist: string;
+ Photo: string;
+ Debut: number;
+ GrammyNominations: number;
+ GrammyAwards: number;
+ HasGrammyAward: boolean;
+ Tours?: any[];
+};
+
+enum GridSection {
+ THEAD = 'thead',
+ TBODY = 'tbody',
+ FOOTER = 'tfoot',
+}
+
+enum ItemAction {
+ Filterable,
+ Sortable,
+ Selectable,
+ Groupable,
+ Collapsible,
+ Expandable,
+ Editable,
+ Always,
+}
+
+type Item = {
+ title: string;
+ subTitle: string;
+ action: ItemAction;
+ active: boolean;
+ completed: boolean;
+};
+
+const makeItem = (
+ title: string,
+ subTitle: string,
+ completed: boolean,
+ action: ItemAction
+): Item => ({ title, subTitle, completed, action, active: action === ItemAction.Always });
+
+const theadKeyCombinations: Item[] = [
+ makeItem('Space', 'select column', false, ItemAction.Selectable),
+ makeItem('Ctrl + ↑/↓', 'sort column asc/desc', false, ItemAction.Sortable),
+ makeItem('Shift + Alt + ←/→', 'group / ungroup column', false, ItemAction.Groupable),
+ makeItem('Alt + arrows', 'expand / collapse column group', false, ItemAction.Collapsible),
+ makeItem('Ctrl + Shift + L', 'open Excel-style filter', false, ItemAction.Filterable),
+ makeItem('Alt + L', 'open Advanced filtering', false, ItemAction.Filterable),
+];
+
+const tbodyKeyCombinations: Item[] = [
+ makeItem('Enter', 'enter edit mode', false, ItemAction.Editable),
+ makeItem('Alt + ←/↑', 'collapse row detail', false, ItemAction.Collapsible),
+ makeItem('Alt + →/↓', 'expand row detail', false, ItemAction.Collapsible),
+ makeItem('Alt + ←/→', 'expand/collapse group row', false, ItemAction.Expandable),
+ makeItem('Ctrl + Home/End', 'jump to first/last cell', false, ItemAction.Always),
+];
+
+const summaryCombinations: Item[] = [
+ makeItem('←', 'move left', false, ItemAction.Always),
+ makeItem('→', 'move right', false, ItemAction.Always),
+ makeItem('Home', 'first summary cell', false, ItemAction.Always),
+ makeItem('End', 'last summary cell', false, ItemAction.Always),
+];
+
+const cloneItems = (items: Item[]) => items.map(i => ({ ...i }));
+
+function enableActions(list: Item[], actions: ItemAction[]) {
+ return list.map(i =>
+ i.action === ItemAction.Always
+ ? { ...i, active: true }
+ : { ...i, active: actions.includes(i.action) }
+ );
+}
+
+function markCompleted(list: Item[], idx: number, value: boolean) {
+ const next = list.slice();
+ next[idx] = { ...next[idx], completed: value };
+ return next;
+}
+
+function checkEditMode(): boolean {
+ const editCells = document.querySelectorAll('.igx-grid__td--editing, .igx-grid__td--cell-editing');
+ const editInputs = document.querySelectorAll('.igx-grid__td input, .igx-grid__td textarea, .igx-grid__td select');
+ const editClasses = document.querySelector('.igx-grid--editing');
+ return editCells.length > 0 || editInputs.length > 0 || !!editClasses;
+}
+
+export default function HierarchicalGridKeyboardNavGuide() {
+ const gridRef = useRef(null);
+
+ const data = SingersData as Singer[];
+ const [gridSection, setGridSection] = useState(GridSection.THEAD);
+ const [items, setItems] = useState- (cloneItems(theadKeyCombinations));
+
+ const headerText = useMemo(() => {
+ if (gridSection === GridSection.THEAD) return 'HEADER COMBINATIONS';
+ if (gridSection === GridSection.TBODY) return 'BODY COMBINATIONS';
+ if (gridSection === GridSection.FOOTER) return 'SUMMARY COMBINATIONS';
+ return '';
+ }, [gridSection]);
+
+ useEffect(() => {
+ const handleGlobalKeyDown = (evt: KeyboardEvent) => {
+ const target = evt.target as HTMLElement;
+ const gridElement = (gridRef.current as any)?.nativeElement || gridRef.current;
+ const isGridFocused = gridElement?.contains?.(target);
+
+ if (!isGridFocused || checkEditMode()) {
+ return;
+ }
+
+ const key = evt.key?.toLowerCase();
+ const { altKey, ctrlKey, shiftKey } = evt;
+
+ if (altKey && (key === 'arrowleft' || key === 'arrowright' || key === 'arrowup' || key === 'arrowdown')) {
+ if (gridSection === GridSection.TBODY) {
+ if (key === 'arrowleft' || key === 'arrowup') {
+ setItems((prev: Item[]) => markCompleted(prev, 1, true));
+ } else if (key === 'arrowright' || key === 'arrowdown') {
+ setItems((prev: Item[]) => markCompleted(prev, 2, true));
+ }
+ }
+ }
+
+ if (ctrlKey && (key === 'home' || key === 'end')) {
+ if (gridSection === GridSection.TBODY) {
+ setItems((prev: Item[]) => markCompleted(prev, 4, true));
+ }
+ }
+
+ if (key === 'enter' && gridSection === GridSection.TBODY) {
+ setTimeout(() => {
+ if (checkEditMode()) {
+ setItems((prev: Item[]) => markCompleted(prev, 0, true));
+ }
+ }, 100);
+ }
+ };
+
+ document.addEventListener('keydown', handleGlobalKeyDown, true);
+ return () => {
+ document.removeEventListener('keydown', handleGlobalKeyDown, true);
+ };
+ }, [gridSection]);
+
+ const updateSectionFromActiveNode = () => {
+ const grid = gridRef.current;
+ const active = (grid as any)?.navigation?.activeNode;
+ const rows = grid?.data?.length ?? data.length;
+
+ if (active && typeof active.row === 'number') {
+ if (active.row < 0) {
+ setGridSection(GridSection.THEAD);
+ setItems(cloneItems(theadKeyCombinations));
+ } else if (active.row >= rows) {
+ setGridSection(GridSection.FOOTER);
+ setItems(cloneItems(summaryCombinations));
+ } else {
+ setGridSection(GridSection.TBODY);
+ setItems(cloneItems(tbodyKeyCombinations));
+ }
+ }
+ };
+
+ const refreshHeaderActions = () => {
+ const grid: any = gridRef.current;
+ if (!grid) return;
+
+ if (gridSection !== GridSection.THEAD) return;
+
+ const active = grid?.navigation?.activeNode;
+ const col = grid.visibleColumns?.find(
+ (c: any) => c.visibleIndex === active?.column && c.level === active?.level
+ );
+
+ const actions: ItemAction[] = [];
+ if (col?.sortable) actions.push(ItemAction.Sortable);
+ if (col?.filterable && !col?.columnGroup) actions.push(ItemAction.Filterable);
+ if (col?.collapsible) actions.push(ItemAction.Collapsible);
+ if (col?.groupable) actions.push(ItemAction.Groupable);
+ if (col?.selectable) actions.push(ItemAction.Selectable);
+ if (col?.editable) actions.push(ItemAction.Editable);
+
+ setItems((prev: Item[]) => enableActions(prev, actions));
+ };
+
+ const onActiveNodeChange = () => {
+ updateSectionFromActiveNode();
+ refreshHeaderActions();
+ };
+
+ const onRowToggle = () => {
+ if (gridSection === GridSection.TBODY) {
+ setItems((prev: Item[]) => markCompleted(prev, 3, true));
+ }
+ };
+
+ const onColumnSelectionChanging = (e: any) => {
+ if (e?.detail?.event?.type === 'keydown') {
+ setItems((prev: Item[]) => markCompleted(prev, 0, true));
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {items.length > 0 && (
+
+ {headerText}
+
+ )}
+ {items.map((c: Item, idx: number) => (
+
+
+
+
{c.title}
+
{c.subTitle}
+
+
{
+ const checked = !!e?.detail;
+ setItems((prev: Item[]) => markCompleted(prev, idx, checked));
+ }}
+ />
+
+
+ ))}
+
+ {items.length === 0 && (
+
+
Use the browser navigation until you reach one of the following grid sections:
+
+ - Header
+ - Body
+ - Summary
+
+
When reached, an action list will be shown.
+
+ )}
+
+
+
+ );
+}
+
+// rendering above component in the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/react-app-env.d.ts b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/react-app-env.d.ts
new file mode 100644
index 0000000000..6431bc5fc6
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/samples/grids/hierarchical-grid/keyboard-navigation-guide/tsconfig.json b/samples/grids/hierarchical-grid/keyboard-navigation-guide/tsconfig.json
new file mode 100644
index 0000000000..8c0d146f95
--- /dev/null
+++ b/samples/grids/hierarchical-grid/keyboard-navigation-guide/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/.eslintrc.js b/samples/grids/tree-grid/keyboard-navigation-guide/.eslintrc.js
new file mode 100644
index 0000000000..7168b71441
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
\ No newline at end of file
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/ReadMe.md b/samples/grids/tree-grid/keyboard-navigation-guide/ReadMe.md
new file mode 100644
index 0000000000..e7047da5aa
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/ReadMe.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Keyboard Custom Navigation feature using [Tree Grid](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/grids/tree-grid/keyboard-custom-navigation
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/package.json b/samples/grids/tree-grid/keyboard-navigation-guide/package.json
new file mode 100644
index 0000000000..9246bcd77a
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "example-ignite-ui-react",
+ "description": "This project provides example of using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "set PORT=4200 && react-scripts --max_old_space_size=10240 start",
+ "build": "react-scripts --max_old_space_size=10240 build ",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-dockmanager": "^1.17.0",
+ "igniteui-react": "^19.2.0",
+ "igniteui-react-core": "^19.0.1",
+ "igniteui-react-grids": "^19.2.0",
+ "igniteui-react-inputs": "^19.0.1",
+ "igniteui-react-layouts": "^19.0.1",
+ "igniteui-webcomponents": "^6.2.0",
+ "lit-html": "^3.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "^5.0.1",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^18.11.7",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "react-app-rewired": "^2.2.1",
+ "typescript": "^4.8.4",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/public/index.html b/samples/grids/tree-grid/keyboard-navigation-guide/public/index.html
new file mode 100644
index 0000000000..e2d3265576
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/sandbox.config.json b/samples/grids/tree-grid/keyboard-navigation-guide/sandbox.config.json
new file mode 100644
index 0000000000..07f53508eb
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
\ No newline at end of file
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/src/EmployeesNestedData.ts b/samples/grids/tree-grid/keyboard-navigation-guide/src/EmployeesNestedData.ts
new file mode 100644
index 0000000000..0728ee874d
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/src/EmployeesNestedData.ts
@@ -0,0 +1,75 @@
+export class EmployeesNestedDataItem {
+ public constructor(init: Partial) {
+ Object.assign(this, init);
+ }
+
+ public ID: number;
+ public Age: number;
+ public Salary: number;
+ public Productivity: number;
+ public City: string;
+ public Country: string;
+ public Phone: string;
+ public HireDate: string;
+ public Name: string;
+ public Title: string;
+ public Employees: EmployeesNestedDataItem_EmployeesItem[];
+
+}
+export class EmployeesNestedDataItem_EmployeesItem {
+ public constructor(init: Partial) {
+ Object.assign(this, init);
+ }
+
+ public Age: number;
+ public Salary: number;
+ public Productivity: number;
+ public City: string;
+ public Country: string;
+ public Phone: string;
+ public HireDate: string;
+ public ID: number;
+ public Name: string;
+ public Title: string;
+
+}
+export class EmployeesNestedData extends Array {
+ public constructor(items: Array | number = -1) {
+ if (Array.isArray(items)) {
+ super(...items);
+ } else {
+ const newItems = [
+ new EmployeesNestedDataItem({ ID: 1, Age: 55, Salary: 80000, Productivity: 90, City: `Berlin`, Country: `Germany`, Phone: `609-202-505`, HireDate: `2008-03-20`, Name: `John Winchester`, Title: `Development Manager`, Employees: [
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 43, Salary: 70000, Productivity: 80, City: `Hamburg`, Country: `Germany`, Phone: `609-444-555`, HireDate: `2011-06-03`, ID: 3, Name: `Michael Burke`, Title: `Senior Software Developer` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 29, Salary: 60000, Productivity: 80, City: `Munich`, Country: `Germany`, Phone: `609-333-444`, HireDate: `2009-06-19`, ID: 2, Name: `Thomas Anderson`, Title: `Senior Software Developer` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 31, Salary: 90000, Productivity: 80, City: `Warasw`, Country: `Poland`, Phone: `609-222-205`, HireDate: `2014-08-18`, ID: 11, Name: `Monica Reyes`, Title: `Software Development Team Lead` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 35, Salary: 70000, Productivity: 70, City: `Koln`, Country: `Germany`, Phone: `609-502-525`, HireDate: `2015-09-17`, ID: 6, Name: `Roland Mendel`, Title: `Senior Software Developer` })]
+ }),
+ new EmployeesNestedDataItem({ ID: 4, Age: 42, Salary: 90000, Productivity: 80, City: `Kielce`, Country: `Poland`, Phone: `609-202-505`, HireDate: `2014-01-22`, Name: `Ana Sanders`, Title: `CEO`, Employees: [
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 44, Salary: 80000, Productivity: 80, City: `Warasw`, Country: `Poland`, Phone: `609-202-505`, HireDate: `2014-04-04`, ID: 14, Name: `Laurence Johnson`, Title: `Director` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 25, Salary: 85000, Productivity: 55, City: `Paris`, Country: `France`, Phone: `609-202-505`, HireDate: `2017-11-09`, ID: 5, Name: `Elizabeth Richards`, Title: `Vice President` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 39, Salary: 88000, Productivity: 88, City: `London`, Country: `UK`, Phone: `609-202-505`, HireDate: `2010-03-22`, ID: 13, Name: `Trevor Ashworth`, Title: `Director` })]
+ }),
+ new EmployeesNestedDataItem({ ID: 18, Age: 49, Salary: 77000, Productivity: 70, City: `Manchester`, Country: `UK`, Phone: `222-555-577`, HireDate: `2014-01-22`, Name: `Victoria Lincoln`, Title: `Senior Accountant`, Employees: [
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 43, Salary: 70000, Productivity: 80, City: `Hamburg`, Country: `Germany`, Phone: `609-444-555`, HireDate: `2011-06-03`, ID: 23, Name: `Thomas Burke`, Title: `Senior Accountant` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 29, Salary: 60000, Productivity: 80, City: `Munich`, Country: `Germany`, Phone: `609-333-444`, HireDate: `2009-06-19`, ID: 22, Name: `Michael Anderson`, Title: `Junior Accountant` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 31, Salary: 90000, Productivity: 80, City: `Warasw`, Country: `Poland`, Phone: `609-222-205`, HireDate: `2014-08-18`, ID: 21, Name: `Roland Reyes`, Title: `Accountant Team Lead` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 35, Salary: 70000, Productivity: 70, City: `Koln`, Country: `Germany`, Phone: `609-502-525`, HireDate: `2015-09-17`, ID: 24, Name: `Monica Mendel`, Title: `Senior Software Developer` })]
+ }),
+ new EmployeesNestedDataItem({ ID: 10, Age: 61, Salary: 85000, Productivity: 890, City: `Lyon`, Country: `France`, Phone: `259-266-887`, HireDate: `2010-01-01`, Name: `Yang Wang`, Title: `Localization Developer`, Employees: [
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 31, Salary: 90000, Productivity: 80, City: `Warasw`, Country: `Poland`, Phone: `609-222-205`, HireDate: `2014-08-18`, ID: 11, Name: `Monica Reyes`, Title: `Software Development Team Lead` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 35, Salary: 70000, Productivity: 70, City: `Koln`, Country: `Germany`, Phone: `609-502-525`, HireDate: `2015-09-17`, ID: 6, Name: `Roland Mendel`, Title: `Senior Software Developer` })]
+ }),
+ new EmployeesNestedDataItem({ ID: 35, Age: 35, Salary: 75000, Productivity: 75, City: `Warasw`, Country: `Poland`, Phone: `688-244-844`, HireDate: `2014-01-22`, Name: `Janine Munoz`, Title: `HR`, Employees: [
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 43, Salary: 70000, Productivity: 80, City: `Hamburg`, Country: `Germany`, Phone: `609-444-555`, HireDate: `2011-06-03`, ID: 3, Name: `Michael Burke`, Title: `Senior Software Developer` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 31, Salary: 90000, Productivity: 80, City: `Warasw`, Country: `Poland`, Phone: `609-222-205`, HireDate: `2014-08-18`, ID: 11, Name: `Monica Reyes`, Title: `Software Development Team Lead` })]
+ }),
+ new EmployeesNestedDataItem({ ID: 10, Age: 49, Salary: 95000, Productivity: 80, City: `Krakow`, Country: `Poland`, Phone: `677-266-555`, HireDate: `2010-01-01`, Name: `Yang Wang`, Title: `Sales Manager`, Employees: [
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 29, Salary: 60000, Productivity: 80, City: `Munich`, Country: `Germany`, Phone: `609-333-444`, HireDate: `2009-06-19`, ID: 2, Name: `Thomas Anderson`, Title: `Senior Software Developer` }),
+ new EmployeesNestedDataItem_EmployeesItem({ Age: 35, Salary: 70000, Productivity: 70, City: `Koln`, Country: `Germany`, Phone: `609-502-525`, HireDate: `2015-09-17`, ID: 6, Name: `Roland Mendel`, Title: `Senior Software Developer` })]
+ }),
+ ];
+ super(...newItems.slice(0));
+ }
+ }
+}
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/src/NwindData.json b/samples/grids/tree-grid/keyboard-navigation-guide/src/NwindData.json
new file mode 100644
index 0000000000..c00b03ec8d
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/src/NwindData.json
@@ -0,0 +1,458 @@
+[
+ {
+ "ProductID": 1,
+ "ProductName": "Chai",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "10 boxes x 20 bags",
+ "UnitPrice": 18,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2012-02-12",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 2,
+ "ProductName": "Chang",
+ "SupplierID": 1,
+ "CategoryID": 1,
+ "QuantityPerUnit": "24 - 12 oz bottles",
+ "UnitPrice": 19,
+ "UnitsInStock": 17,
+ "UnitsOnOrder": 40,
+ "ReorderLevel": 25,
+ "Discontinued": true,
+ "OrderDate": "2003-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 3,
+ "ProductName": "Aniseed Syrup",
+ "SupplierID": 1,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 550 ml bottles",
+ "UnitPrice": 10,
+ "UnitsInStock": 13,
+ "UnitsOnOrder": 70,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2006-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 4,
+ "ProductName": "Chef Antons Cajun Seasoning",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "48 - 6 oz jars",
+ "UnitPrice": 22,
+ "UnitsInStock": 53,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2016-03-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 5,
+ "ProductName": "Chef Antons Gumbo Mix",
+ "SupplierID": 2,
+ "CategoryID": 2,
+ "QuantityPerUnit": "36 boxes",
+ "UnitPrice": 21.35,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2011-11-11",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 6,
+ "ProductName": "Grandmas Boysenberry Spread",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 8 oz jars",
+ "UnitPrice": 25,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 25,
+ "Discontinued": false,
+ "OrderDate": "2017-12-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 7,
+ "ProductName": "Uncle Bobs Organic Dried Pears",
+ "SupplierID": 3,
+ "CategoryID": 7,
+ "QuantityPerUnit": "12 - 1 lb pkgs.",
+ "UnitPrice": 30,
+ "UnitsInStock": 150,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2016-07-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 8,
+ "ProductName": "Northwoods Cranberry Sauce",
+ "SupplierID": 3,
+ "CategoryID": 2,
+ "QuantityPerUnit": "12 - 12 oz jars",
+ "UnitPrice": 40,
+ "UnitsInStock": 6,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2018-01-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 9,
+ "ProductName": "Mishi Kobe Niku",
+ "SupplierID": 4,
+ "CategoryID": 6,
+ "QuantityPerUnit": "18 - 500 g pkgs.",
+ "UnitPrice": 97,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2010-02-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 10,
+ "ProductName": "Ikura",
+ "SupplierID": 4,
+ "CategoryID": 8,
+ "QuantityPerUnit": "12 - 200 ml jars",
+ "UnitPrice": 31,
+ "UnitsInStock": 31,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2008-05-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 11,
+ "ProductName": "Queso Cabrales",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "1 kg pkg.",
+ "UnitPrice": 21,
+ "UnitsInStock": 22,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 30,
+ "Discontinued": false,
+ "OrderDate": "2009-01-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Fun-Tasty Co.",
+ "LastInventory": "2018-06-12"
+ },
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 12,
+ "ProductName": "Queso Manchego La Pastora",
+ "SupplierID": 5,
+ "CategoryID": 4,
+ "QuantityPerUnit": "10 - 500 g pkgs.",
+ "UnitPrice": 38,
+ "UnitsInStock": 86,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2015-11-17",
+ "Rating": 3,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 13,
+ "ProductName": "Konbu",
+ "SupplierID": 6,
+ "CategoryID": 8,
+ "QuantityPerUnit": "2 kg box",
+ "UnitPrice": 6,
+ "UnitsInStock": 24,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2015-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 14,
+ "ProductName": "Tofu",
+ "SupplierID": 6,
+ "CategoryID": 7,
+ "QuantityPerUnit": "40 - 100 g pkgs.",
+ "UnitPrice": 23.25,
+ "UnitsInStock": 35,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2017-06-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ }
+ ]
+ },
+ {
+ "ProductID": 15,
+ "ProductName": "Genen Shouyu",
+ "SupplierID": 6,
+ "CategoryID": 2,
+ "QuantityPerUnit": "24 - 250 ml bottles",
+ "UnitPrice": 15.5,
+ "UnitsInStock": 39,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2014-03-17",
+ "Rating": 4,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ },
+ {
+ "Shop": "Wall Market",
+ "LastInventory": "2018-12-06"
+ }
+ ]
+ },
+ {
+ "ProductID": 16,
+ "ProductName": "Pavlova",
+ "SupplierID": 7,
+ "CategoryID": 3,
+ "QuantityPerUnit": "32 - 500 g boxes",
+ "UnitPrice": 17.45,
+ "UnitsInStock": 29,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 10,
+ "Discontinued": false,
+ "OrderDate": "2018-03-28",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ },
+ {
+ "Shop": "Street Market",
+ "LastInventory": "2018-12-12"
+ },
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ }
+ ]
+ },
+ {
+ "ProductID": 17,
+ "ProductName": "Alice Mutton",
+ "SupplierID": 7,
+ "CategoryID": 6,
+ "QuantityPerUnit": "20 - 1 kg tins",
+ "UnitPrice": 39,
+ "UnitsInStock": 0,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": true,
+ "OrderDate": "2015-08-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Farmer Market",
+ "LastInventory": "2018-04-04"
+ }
+ ]
+ },
+ {
+ "ProductID": 18,
+ "ProductName": "Carnarvon Tigers",
+ "SupplierID": 7,
+ "CategoryID": 8,
+ "QuantityPerUnit": "16 kg pkg.",
+ "UnitPrice": 62.5,
+ "UnitsInStock": 42,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-09-27",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "24/7 Market",
+ "LastInventory": "2018-11-11"
+ },
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ },
+ {
+ "ProductID": 19,
+ "ProductName": "Teatime Chocolate Biscuits",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "",
+ "UnitPrice": 9.2,
+ "UnitsInStock": 25,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 5,
+ "Discontinued": false,
+ "OrderDate": "2001-03-17",
+ "Rating": 2,
+ "Locations": [
+ {
+ "Shop": "Local Market",
+ "LastInventory": "2018-07-03"
+ }
+ ]
+ },
+ {
+ "ProductID": 20,
+ "ProductName": "Sir Rodneys Marmalade",
+ "SupplierID": 8,
+ "CategoryID": 3,
+ "QuantityPerUnit": "4 - 100 ml jars",
+ "UnitPrice": 4.5,
+ "UnitsInStock": 40,
+ "UnitsOnOrder": 30,
+ "ReorderLevel": 0,
+ "Discontinued": false,
+ "OrderDate": "2005-03-17",
+ "Rating": 5,
+ "Locations": [
+ {
+ "Shop": "Super Market",
+ "LastInventory": "2018-09-09"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/src/index.css b/samples/grids/tree-grid/keyboard-navigation-guide/src/index.css
new file mode 100644
index 0000000000..3b330b391d
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/src/index.css
@@ -0,0 +1,7 @@
+/* shared styles are loaded from: */
+/* https://static.infragistics.com/xplatform/css/samples */
+
+ #grid {
+ --ig-size: var(--ig-size-medium);
+ }
+
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/src/index.tsx b/samples/grids/tree-grid/keyboard-navigation-guide/src/index.tsx
new file mode 100644
index 0000000000..dfe7ec8ea9
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/src/index.tsx
@@ -0,0 +1,380 @@
+
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import {
+ IgrTreeGrid,
+ IgrColumn,
+ IgrPaginator
+} from 'igniteui-react-grids';
+import { IgrCheckbox } from 'igniteui-react';
+import { IgrList, IgrListItem, IgrListHeader } from 'igniteui-react';
+import { EmployeesNestedDataItem, EmployeesNestedData } from './EmployeesNestedData';
+
+import 'igniteui-react-grids/grids/themes/light/bootstrap.css';
+
+type Employee = EmployeesNestedDataItem;
+enum GridSection {
+ THEAD = 'thead',
+ TBODY = 'tbody',
+ FOOTER = 'tfoot',
+}
+
+enum ItemAction {
+ Filterable,
+ Sortable,
+ Selectable,
+ Collapsible,
+ Editable,
+ Always,
+}
+
+type Item = {
+ title: string;
+ subTitle: string;
+ action: ItemAction;
+ active: boolean;
+ completed: boolean;
+};
+
+const makeItem = (
+ title: string,
+ subTitle: string,
+ completed: boolean,
+ action: ItemAction
+): Item => ({ title, subTitle, completed, action, active: action === ItemAction.Always });
+
+const theadKeyCombinations: Item[] = [
+ makeItem('Space', 'select column', false, ItemAction.Selectable),
+ makeItem('Ctrl + ↑/↓', 'sort column asc/desc', false, ItemAction.Sortable),
+ makeItem('Ctrl + Shift + L', 'open Excel-style filter', false, ItemAction.Filterable),
+ makeItem('Alt + L', 'open Advanced filtering', false, ItemAction.Filterable),
+];
+
+const tbodyKeyCombinations: Item[] = [
+ makeItem('Enter', 'enter edit mode', false, ItemAction.Editable),
+ makeItem('Alt + ←/↑', 'collapse row detail', false, ItemAction.Collapsible),
+ makeItem('Alt + →/↓', 'expand row detail', false, ItemAction.Collapsible),
+ makeItem('Ctrl + Home/End', 'jump to first/last cell', false, ItemAction.Always),
+];
+
+const summaryCombinations: Item[] = [
+ makeItem('←', 'move left', false, ItemAction.Always),
+ makeItem('→', 'move right', false, ItemAction.Always),
+ makeItem('Home', 'first summary cell', false, ItemAction.Always),
+ makeItem('End', 'last summary cell', false, ItemAction.Always),
+];
+
+const cloneItems = (items: Item[]) => items.map(i => ({ ...i }));
+
+function enableActions(list: Item[], actions: ItemAction[]) {
+ return list.map(i =>
+ i.action === ItemAction.Always
+ ? { ...i, active: true }
+ : { ...i, active: actions.includes(i.action) }
+ );
+}
+
+function markCompleted(list: Item[], idx: number, value: boolean) {
+ const next = list.slice();
+ next[idx] = { ...next[idx], completed: value };
+ return next;
+}
+
+export default function TreeGridKeyboardNavGuide() {
+ const gridRef = useRef(null);
+ const lastKeyRef = useRef('');
+ const isInEditModeRef = useRef(false);
+
+ const [data] = useState(() => new EmployeesNestedData());
+ const [gridSection, setGridSection] = useState(GridSection.THEAD);
+ const [items, setItems] = useState- (cloneItems(theadKeyCombinations));
+
+ const checkEditMode = () => {
+ const editingCell = document.querySelector('.igx-grid__td--editing, .igc-grid__td--editing');
+ const gridElement = (gridRef.current as any)?.nativeElement;
+ const inputInGrid = gridElement?.querySelector('input, textarea, select');
+ return !!(editingCell || inputInGrid);
+ };
+
+ useEffect(() => {
+ const handleGlobalKeyDown = (evt: KeyboardEvent) => {
+ const key = evt.key?.toLowerCase();
+
+ const gridElement = (gridRef.current as any)?.nativeElement;
+ const focusedElement = document.activeElement;
+
+ if (gridElement && (gridElement.contains(focusedElement) || gridElement === focusedElement)) {
+ if (evt.altKey && (key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright')) {
+ lastKeyRef.current = key;
+ }
+
+ if (evt.ctrlKey && (key === 'home' || key === 'end')) {
+ lastKeyRef.current = key;
+
+ if (gridSection === GridSection.TBODY) {
+ setTimeout(() => {
+ setItems((prev: Item[]) => markCompleted(prev, 3, true));
+ }, 0);
+ }
+ }
+
+ if (key === 'enter') {
+ const focusedElement = document.activeElement;
+
+ const focusedIsInput = focusedElement && (
+ focusedElement.tagName === 'INPUT' ||
+ focusedElement.tagName === 'TEXTAREA' ||
+ focusedElement.tagName === 'SELECT'
+ );
+
+ const focusedCellInEditMode = focusedElement && focusedElement.closest('.igx-grid__td--editing, .igc-grid__td--editing');
+
+ const currentlyInEditMode = !!(focusedIsInput || focusedCellInEditMode);
+
+ if (!currentlyInEditMode && gridSection === GridSection.TBODY) {
+ setItems((prev: Item[]) => markCompleted(prev, 0, true));
+ }
+
+ isInEditModeRef.current = currentlyInEditMode;
+ }
+
+ if (key === 'escape') {
+ setTimeout(() => {
+ isInEditModeRef.current = checkEditMode();
+ }, 10);
+ }
+ }
+ };
+
+ document.addEventListener('keydown', handleGlobalKeyDown, true);
+
+ return () => {
+ document.removeEventListener('keydown', handleGlobalKeyDown, true);
+ };
+ }, [gridSection]);
+
+ const headerText = useMemo(() => {
+ if (gridSection === GridSection.THEAD) return 'HEADER COMBINATIONS';
+ if (gridSection === GridSection.TBODY) return 'BODY COMBINATIONS';
+ if (gridSection === GridSection.FOOTER) return 'SUMMARY COMBINATIONS';
+ return '';
+ }, [gridSection]);
+
+ const updateSectionFromActiveNode = () => {
+ const grid = gridRef.current;
+ const active = (grid as any)?.navigation?.activeNode;
+ const rows = grid?.data?.length ?? data.length;
+ if (active && typeof active.row === 'number') {
+ if (active.row < 0) {
+ setGridSection(GridSection.THEAD);
+ if (gridSection !== GridSection.THEAD) {
+ setItems(cloneItems(theadKeyCombinations));
+ }
+ } else if (active.row >= rows) {
+ setGridSection(GridSection.FOOTER);
+ if (gridSection !== GridSection.FOOTER) {
+ setItems(cloneItems(summaryCombinations));
+ }
+ } else {
+ setGridSection(GridSection.TBODY);
+ if (gridSection !== GridSection.TBODY) {
+ setItems(cloneItems(tbodyKeyCombinations));
+ }
+ }
+ }
+ };
+
+ const onGridKeyDown = (evt: any) => {
+ const key = evt.key?.toLowerCase();
+ if (!key) return;
+ if (key === 'tab') return;
+
+ if ((key === 'l' && evt.altKey) ||
+ (key === 'l' && evt.ctrlKey && evt.shiftKey) ||
+ ((key === 'arrowup' || key === 'arrowdown') && evt.ctrlKey) ||
+ ((key === 'end' || key === 'home') && evt.ctrlKey)) {
+ evt.preventDefault();
+ }
+
+ updateSectionFromActiveNode();
+
+ setItems((prev: Item[]) => {
+ let next = prev.slice();
+
+ if (gridSection === GridSection.FOOTER) {
+ switch (key) {
+ case 'end':
+ next = markCompleted(next, 3, true);
+ break;
+ case 'home':
+ next = markCompleted(next, 2, true);
+ break;
+ case 'arrowleft':
+ next = markCompleted(next, 0, true);
+ break;
+ case 'arrowright':
+ next = markCompleted(next, 1, true);
+ break;
+ }
+ return next;
+ }
+
+ if (gridSection === GridSection.THEAD) {
+ if (key === 'l' && evt.altKey) {
+ next = markCompleted(next, 3, true);
+ return next;
+ }
+ if (key === 'l' && evt.ctrlKey && evt.shiftKey) {
+ next = markCompleted(next, 2, true);
+ }
+ if ((key === 'arrowup' || key === 'arrowdown') && evt.ctrlKey) {
+ next = markCompleted(next, 1, true);
+ }
+ if (key === ' ' || key === 'spacebar') {
+ next = markCompleted(next, 0, true);
+ }
+ }
+
+ return next;
+ });
+ };
+
+ const refreshHeaderActions = () => {
+ const grid: any = gridRef.current;
+ if (!grid) return;
+
+ if (gridSection !== GridSection.THEAD) return;
+
+ const active = grid?.navigation?.activeNode;
+ const col = grid.visibleColumns?.find(
+ (c: any) => c.visibleIndex === active?.column && c.level === active?.level
+ );
+
+ const actions: ItemAction[] = [];
+ if (col?.sortable) actions.push(ItemAction.Sortable);
+ if (col?.filterable && !col?.columnGroup) actions.push(ItemAction.Filterable);
+ if (col?.selectable) actions.push(ItemAction.Selectable);
+
+ setItems((prev: Item[]) => enableActions(prev, actions));
+ };
+
+ const onActiveNodeChange = () => {
+ updateSectionFromActiveNode();
+ refreshHeaderActions();
+
+ setTimeout(() => {
+ isInEditModeRef.current = checkEditMode();
+ }, 10);
+ };
+
+ useEffect(() => {
+ const handleGlobalClick = () => {
+ setTimeout(() => {
+ isInEditModeRef.current = checkEditMode();
+ }, 10);
+ };
+
+ document.addEventListener('click', handleGlobalClick, true);
+
+ return () => {
+ document.removeEventListener('click', handleGlobalClick, true);
+ };
+ }, []);
+
+ const onRowToggle = () => {
+ if (gridSection === GridSection.TBODY || gridSection === GridSection.FOOTER) {
+ const lastKey = lastKeyRef.current;
+ setItems((prev: Item[]) => {
+ if (lastKey === 'arrowup' || lastKey === 'arrowleft') {
+ return markCompleted(prev, 1, true);
+ } else if (lastKey === 'arrowdown' || lastKey === 'arrowright') {
+ return markCompleted(prev, 2, true);
+ }
+ return prev;
+ });
+ setTimeout(() => {
+ lastKeyRef.current = '';
+ }, 100);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {/* Tree grid columns for Employees */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Right-side list showing active/available shortcuts */}
+
+
+ {items.length > 0 && (
+
+ {headerText}
+
+ )}
+ {items.map((c: Item, idx: number) => (
+
+
+
+
{c.title}
+
{c.subTitle}
+
+
{
+ const checked = !!e?.detail;
+ setItems((prev: Item[]) => markCompleted(prev, idx, checked));
+ }}
+ />
+
+
+ ))}
+
+ {items.length === 0 && (
+
+
Use the browser navigation until you reach one of the following grid sections:
+
+ - Header
+ - Body
+ - Summary
+
+
When reached, an action list will be shown.
+
+ )}
+
+
+
+ );
+}
+
+// rendering above component in the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/src/react-app-env.d.ts b/samples/grids/tree-grid/keyboard-navigation-guide/src/react-app-env.d.ts
new file mode 100644
index 0000000000..6431bc5fc6
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/samples/grids/tree-grid/keyboard-navigation-guide/tsconfig.json b/samples/grids/tree-grid/keyboard-navigation-guide/tsconfig.json
new file mode 100644
index 0000000000..8c0d146f95
--- /dev/null
+++ b/samples/grids/tree-grid/keyboard-navigation-guide/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}