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. + + + + + + View Docs + + + View Code + + + Run Sample + + + Run Sample + + + + +## 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. + + + + + + View Docs + + + View Code + + + Run Sample + + + Run Sample + + + + +## 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. + + + + + + View Docs + + + View Code + + + Run Sample + + + Run Sample + + + + +## 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" + ] +}