diff --git a/apps/website/pages/components/data-grid/index.tsx b/apps/website/pages/components/data-grid/index.tsx new file mode 100644 index 0000000000..7dda5ffb71 --- /dev/null +++ b/apps/website/pages/components/data-grid/index.tsx @@ -0,0 +1,21 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import DataGridCodePage from "screens/components/data-grid/code/DataGridCodePage"; +import DataGridPageLayout from "screens/components/data-grid/DatagridPageLayout"; + +const Usage = () => { + return ( + <> + + Data Grid — Halstack Design System + + + + ); +}; + +Usage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Usage; diff --git a/apps/website/pages/components/data-grid/specifications.tsx b/apps/website/pages/components/data-grid/specifications.tsx new file mode 100644 index 0000000000..76a440f795 --- /dev/null +++ b/apps/website/pages/components/data-grid/specifications.tsx @@ -0,0 +1,21 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import DataGridSpecsPage from "screens/components/data-grid/specs/DataGridSpecsPage"; +import DataGridPageLayout from "screens/components/data-grid/DatagridPageLayout"; + +const Specifications = () => { + return ( + <> + + Data Grid Specs — Halstack Design System + + + + ); +}; + +Specifications.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Specifications; diff --git a/apps/website/pages/components/data-grid/usage.tsx b/apps/website/pages/components/data-grid/usage.tsx new file mode 100644 index 0000000000..c426cb7228 --- /dev/null +++ b/apps/website/pages/components/data-grid/usage.tsx @@ -0,0 +1,21 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import DataGridUsagePage from "screens/components/data-grid/usage/DataGridUsagePage"; +import DataGridPageLayout from "../../../screens/components/data-grid/DatagridPageLayout"; + +const Usage = () => { + return ( + <> + + Data Grid Usage — Halstack Design System + + + + ); +}; + +Usage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Usage; diff --git a/apps/website/screens/common/componentsList.json b/apps/website/screens/common/componentsList.json index 634ac1fd00..cddd7c5346 100644 --- a/apps/website/screens/common/componentsList.json +++ b/apps/website/screens/common/componentsList.json @@ -37,6 +37,11 @@ "path": "/components/contextual-menu", "status": "new" }, + { + "label": "Data Grid", + "path": "/components/data-grid", + "status": "experimental" + }, { "label": "Date Input", "path": "/components/date-input", diff --git a/apps/website/screens/common/themes/advanced-theme.json b/apps/website/screens/common/themes/advanced-theme.json index 07dc71315a..132ccfd424 100644 --- a/apps/website/screens/common/themes/advanced-theme.json +++ b/apps/website/screens/common/themes/advanced-theme.json @@ -350,6 +350,43 @@ "iconColor": "#333333", "iconSize": "16px" }, + "dataGrid": { + "rowSeparatorThickness": "1px", + "rowSeparatorStyle": "solid", + "rowSeparatorColor": "#cccccc", + "dataBackgroundColor": "#ffffff", + "dataFontFamily": "Open Sans, sans-serif", + "dataFontSize": "0.875rem", + "dataFontStyle": "normal", + "dataFontWeight": "400", + "dataFontColor": "#000000", + "dataFontTextTransform": "none", + "dataPaddingRight": "0.5rem", + "dataPaddingLeft": "0.5rem", + "dataRowHeight": "36", + "dataTextLineHeight": "normal", + "headerBackgroundColor": "#5f249f", + "headerBorderRadius": "4px", + "headerFontFamily": "Open Sans, sans-serif", + "headerFontSize": "0.875rem", + "headerFontStyle": "normal", + "headerFontWeight": "bold", + "headerFontColor": "#ffffff", + "headerFontTextTransform": "none", + "headerPaddingRight": "0.5rem", + "headerPaddingLeft": "0.5rem", + "headerRowHeight": "36", + "headerTextLineHeight": "normal", + "headerCheckboxBackgroundColorChecked": "#ffffff", + "headerCheckboxHoverBackgroundColorChecked": "#e6e6e6", + "headerCheckboxBorderColor": "#ffffff", + "headerCheckboxHoverBorderColor": "#ffffff", + "headerCheckboxCheckColor": "#5f249f", + "summaryRowHeight": "36", + "focusColor": "#0095ff", + "scrollBarThumbColor": "#666666", + "scrollBarTrackColor": "#cccccc" + }, "dateInput": { "pickerBackgroundColor": "#ffffff", "pickerFontColor": "#000000", diff --git a/apps/website/screens/common/themes/opinionated-theme.json b/apps/website/screens/common/themes/opinionated-theme.json index 68712a8eb2..8e31e4f4d3 100644 --- a/apps/website/screens/common/themes/opinionated-theme.json +++ b/apps/website/screens/common/themes/opinionated-theme.json @@ -33,6 +33,11 @@ "fontColor": "#333333", "iconColor": "#333333" }, + "dataGrid": { + "baseColor": "#5f249f", + "headerFontColor": "#ffffff", + "cellFontColor": "#000000" + }, "dateInput": { "baseColor": "#5f249f", "selectedFontColor": "#ffffff" diff --git a/apps/website/screens/components/data-grid/DatagridPageLayout.tsx b/apps/website/screens/components/data-grid/DatagridPageLayout.tsx new file mode 100644 index 0000000000..8dbeea9ace --- /dev/null +++ b/apps/website/screens/components/data-grid/DatagridPageLayout.tsx @@ -0,0 +1,32 @@ +import { DxcParagraph, DxcFlex } from "@dxc-technology/halstack-react"; +import PageHeading from "@/common/PageHeading"; +import TabsPageHeading from "@/common/TabsPageLayout"; +import ComponentHeading from "@/common/ComponentHeading"; + +const DataGridPageHeading = ({ children }: { children: React.ReactNode }) => { + const tabs = [ + { label: "Code", path: "/components/data-grid" }, + // { label: "Usage", path: "/components/data-grid/usage" }, + // { label: "Specifications", path: "/components/data-grid/specifications" }, + ]; + + return ( + + + + + + A data grid is a component designed to display large volumes in a structured and organized manner. It + structures data into rows and columns, making it easy for users to visualize, analyze, and interact with the + information. The data grid also improves user experience by providing features like sorting, filtering, and + editing. + + + + + {children} + + ); +}; + +export default DataGridPageHeading; diff --git a/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx new file mode 100644 index 0000000000..5f8780a135 --- /dev/null +++ b/apps/website/screens/components/data-grid/code/DataGridCodePage.tsx @@ -0,0 +1,220 @@ +import { DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import DocFooter from "@/common/DocFooter"; +import Code from "@/common/Code"; +import Example from "@/common/example/Example"; +import TableCode, { ExtendedTableCode } from "@/common/TableCode"; +import StatusBadge from "@/common/StatusBadge"; +import basicUsage from "./examples/basicUsage"; +import selectable from "./examples/selectable"; +import expandable from "./examples/expandable"; +import hierarchical from "./examples/hierarchical"; +import hierarchicalSelectable from "./examples/hierarchicalSelectable"; + +const columnsTypeString = `{ + key: string; + label: string; + resizable?: boolean; + sortable?: boolean; + draggable?: boolean; + textEditable?: boolean; + summaryKey?: string; + alignment?: "left" | "right" | "center"; +}`; + +const GridRowTypeString = `{ + [key: string]: React.ReactNode | undefined; +}`; + +const HierarchyGridRowTypeString = `GridRow & { + childRows?: HierarchyGridRow[] | GridRow[]; +}`; + +const ExpandableGridRowTypeString = `GridRow & { + expandedContent?: React.ReactNode; + expandedContentHeight?: number; +}`; + +const sections = [ + { + title: "Props", + content: ( + + + + Name + Type + Description + Default + + + + + + + columns + + +

+ GridColumn[] + being GridColumn: +

+ {columnsTypeString} + + + Each GridColumn object has the following properties: +
    +
  • + key: Key that will be rendered from each row in rows. +
  • +
  • + label: Label that will be used for the column header. +
  • +
  • + resizable: Whether the column is resizable or not. +
  • +
  • + sortable: Whether the column is sortable or not. +
  • +
  • + draggable: Whether the column can be dragged or not to another position or not. +
  • +
  • + textEditable: Whether the column cells are editable or not. +
  • +
  • + summaryKey: Value that will be rendered from the summaryRow +
  • +
  • + alignment: St sets the alignment inside the cells. +
  • +
+ + - + + + + + rows + + + GridRow[] | HierarchyGridRow[] | ExpandableGridRow[] +

Each one of them being in order:

+

+ {GridRowTypeString} +

+

+ {HierarchyGridRowTypeString} +

+

+ {ExpandableGridRowTypeString} +

+ + + List of rows that will be rendered in each cell based on the key in each column. + + - + + + expandable + + boolean + + Whether the rows can expand or not. + - + + + summaryRow + + GridRow + + Extra row that will be always visible. + - + + + selectable + + boolean + + Whether the rows are selectable or not. + - + + + selectedRows + + {`Set`} + + + Set of selected rows. This prop is mandatory if selectable is set to true. The{" "} + uniqueRowId key will be used to identify the each row. + + - + + + onSelectRows + + {`(selectedRows: Set) => void`} + + + Function called whenever the selected values changes. This prop is mandatory if selectable is + set to true.The uniqueRowId key will be used to identify the rows. + + - + + + uniqueRowId + + string + + + This prop indicates the unique key that can be used to identify each row. This prop is mandatory if{" "} + selectable is set to true, expandable is set to true or rows is of + type HierarchyGridRow[]. + + - + + +
+ ), + }, + + { + title: "Examples", + subSections: [ + { + title: "Basic usage", + content: , + }, + { + title: "Selectable data grid", + content: , + }, + { + title: "Expandable data grid", + content: , + }, + { + title: "Hierarchical data grid", + content: , + }, + { + title: "Hierarchical and selectable data grid", + content: , + }, + ], + }, +]; + +const DataGridCodePage = () => { + return ( + + + + + + + ); +}; + +export default DataGridCodePage; diff --git a/apps/website/screens/components/data-grid/code/examples/basicUsage.ts b/apps/website/screens/components/data-grid/code/examples/basicUsage.ts new file mode 100644 index 0000000000..fcff29029d --- /dev/null +++ b/apps/website/screens/components/data-grid/code/examples/basicUsage.ts @@ -0,0 +1,53 @@ +import { DxcDataGrid, DxcInset } from "@dxc-technology/halstack-react"; + +const code = `() => { + const columns = [ + { + key: "id", + label: "ID", + resizable: true, + sortable: true, + draggable: true, + }, + { + key: "complete", + label: "% Complete", + resizable: true, + sortable: true, + draggable: true, + alignment: "center", + }, + ]; + + const rows = [ + { + id: 1, + complete: 46, + }, + { + id: 2, + complete: 51, + }, + { + id: 3, + complete: 40, + }, + { + id: 4, + complete: 10, + }, + + ]; + return ( + + + + ); +}`; + +const scope = { + DxcDataGrid, + DxcInset, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/data-grid/code/examples/expandable.ts b/apps/website/screens/components/data-grid/code/examples/expandable.ts new file mode 100644 index 0000000000..0f770a0d57 --- /dev/null +++ b/apps/website/screens/components/data-grid/code/examples/expandable.ts @@ -0,0 +1,56 @@ +import { DxcDataGrid, DxcInset } from "@dxc-technology/halstack-react"; + +const code = `() => { + const columns = [ + { + key: "id", + label: "ID", + resizable: true, + sortable: true, + draggable: true, + }, + { + key: "complete", + label: "% Complete", + resizable: true, + sortable: true, + draggable: true, + alignment: "center", + }, + ]; + + const rows = [ + { + id: 1, + complete: 46, + expandedContent: "Expanded content" + }, + { + id: 2, + complete: 51, + expandedContent: "Expanded content", + expandedContentHeight: 100 + }, + { + id: 3, + complete: 40, + }, + { + id: 4, + complete: 10, + }, + + ]; + return ( + + + + ); +}`; + +const scope = { + DxcDataGrid, + DxcInset, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/data-grid/code/examples/hierarchical.ts b/apps/website/screens/components/data-grid/code/examples/hierarchical.ts new file mode 100644 index 0000000000..42fd8d49a6 --- /dev/null +++ b/apps/website/screens/components/data-grid/code/examples/hierarchical.ts @@ -0,0 +1,93 @@ +import { DxcDataGrid, DxcInset } from "@dxc-technology/halstack-react"; + +const code = `() => { + const columns = [ + { + key: "name", + label: "Label", + summaryKey: "label" + }, + { + key: "value", + label: "Value", + alignment: "center", + summaryKey: "total" + }, + ]; + + const rows = [ + { + name: "Root Node 1", + value: "1", + id: "a", + childRows: [ + { + name: "Child Node 1.1", + value: "1.1", + id: "aa", + childRows: [ + { + name: "Grandchild Node 1.1.1", + value: "1.1.1", + id: "aaa", + }, + { + name: "Grandchild Node 1.1.2", + value: "1.1.2", + id: "aab", + }, + ], + }, + { + name: "Child Node 1.2", + value: "1.2", + id: "ab", + }, + ], + }, + { + name: "Root Node 2", + value: "2", + id: "b", + childRows: [ + { + name: "Child Node 2.1", + value: "2.1", + id: "ba", + childRows: [ + { + name: "Grandchild Node 2.1.1", + value: "2.1.1", + id: "baa", + }, + ], + }, + { + name: "Child Node 2.2", + value: "2.2", + id: "bb", + }, + { + name: "Child Node 2.3", + value: "2.3", + id: "bc", + }, + ], + }, + ]; + + const summaryRow = { label: "Total", total: 100, id: "summary" } + + return ( + + + + ); +}`; + +const scope = { + DxcDataGrid, + DxcInset, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/data-grid/code/examples/hierarchicalSelectable.ts b/apps/website/screens/components/data-grid/code/examples/hierarchicalSelectable.ts new file mode 100644 index 0000000000..19918de61c --- /dev/null +++ b/apps/website/screens/components/data-grid/code/examples/hierarchicalSelectable.ts @@ -0,0 +1,101 @@ +import { DxcDataGrid, DxcInset } from "@dxc-technology/halstack-react"; +import { useState } from "react"; + +const code = `() => { + const columns = [ + { + key: "name", + label: "Label", + summaryKey: "label" + }, + { + key: "value", + label: "Value", + alignment: "center", + summaryKey: "total" + }, + ]; + + const rows = [ + { + name: "Root Node 1", + value: "1", + id: "a", + childRows: [ + { + name: "Child Node 1.1", + value: "1.1", + id: "aa", + childRows: [ + { + name: "Grandchild Node 1.1.1", + value: "1.1.1", + id: "aaa", + }, + { + name: "Grandchild Node 1.1.2", + value: "1.1.2", + id: "aab", + }, + ], + }, + { + name: "Child Node 1.2", + value: "1.2", + id: "ab", + }, + ], + }, + { + name: "Root Node 2", + value: "2", + id: "b", + childRows: [ + { + name: "Child Node 2.1", + value: "2.1", + id: "ba", + childRows: [ + { + name: "Grandchild Node 2.1.1", + value: "2.1.1", + id: "baa", + }, + ], + }, + { + name: "Child Node 2.2", + value: "2.2", + id: "bb", + }, + { + name: "Child Node 2.3", + value: "2.3", + id: "bc", + }, + ], + }, + ]; + + const [selectedRows, setSelectedRows] = useState(new Set()); + return ( + + + + ); +}`; + +const scope = { + DxcDataGrid, + DxcInset, + useState, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/data-grid/code/examples/selectable.ts b/apps/website/screens/components/data-grid/code/examples/selectable.ts new file mode 100644 index 0000000000..982447d3fc --- /dev/null +++ b/apps/website/screens/components/data-grid/code/examples/selectable.ts @@ -0,0 +1,64 @@ +import { DxcDataGrid, DxcInset } from "@dxc-technology/halstack-react"; +import { useState } from "react"; + +const code = `() => { + const columns = [ + { + key: "id", + label: "ID", + resizable: true, + sortable: true, + draggable: true, + }, + { + key: "complete", + label: "% Complete", + resizable: true, + sortable: true, + draggable: true, + alignment: "center", + }, + ]; + + const rows = [ + { + id: 1, + complete: 46, + }, + { + id: 2, + complete: 51, + }, + { + id: 3, + complete: 40, + }, + { + id: 4, + complete: 10, + }, + + ]; + + const [selectedRows, setSelectedRows] = useState(new Set()); + return ( + + + + ); +}`; + +const scope = { + DxcDataGrid, + DxcInset, + useState, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/data-grid/specs/DataGridSpecsPage.tsx b/apps/website/screens/components/data-grid/specs/DataGridSpecsPage.tsx new file mode 100644 index 0000000000..e82e98cbd8 --- /dev/null +++ b/apps/website/screens/components/data-grid/specs/DataGridSpecsPage.tsx @@ -0,0 +1,72 @@ +import { DxcParagraph, DxcBulletedList, DxcTable, DxcFlex, DxcLink } from "@dxc-technology/halstack-react"; +import Image from "@/common/Image"; +import Link from "next/link"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import DocFooter from "@/common/DocFooter"; +import Figure from "@/common/Figure"; +import Code from "@/common/Code"; + +const sections = [ + { + title: "Date input", + content: <>TODO, + }, + { + title: "Design tokens", + subSections: [ + { + title: "TODO", + content: ( + + + + Component token + Element + Core token + Value + + + + + + + + ), + }, + ], + }, + { + title: "Accessibility", + subSections: [ + { + title: "WAI-ARIA", + content: ( + <> + + + WAI-ARIA authoring practices -{" "} + + Data Grid Examples + + + + + ), + }, + ], + }, +]; + +const DataGridSpecsPage = () => { + return ( + + + + + + + ); +}; + +export default DataGridSpecsPage; diff --git a/apps/website/screens/components/data-grid/usage/DataGridUsagePage.tsx b/apps/website/screens/components/data-grid/usage/DataGridUsagePage.tsx new file mode 100644 index 0000000000..343e403d4a --- /dev/null +++ b/apps/website/screens/components/data-grid/usage/DataGridUsagePage.tsx @@ -0,0 +1,28 @@ +import { DxcBulletedList, DxcFlex } from "@dxc-technology/halstack-react"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import DocFooter from "@/common/DocFooter"; + +const sections = [ + { + title: "Usage", + content: ( + + Use the data grid (TODO). + + ), + }, +]; + +const DataGridUsagePage = () => { + return ( + + + + + + + ); +}; + +export default DataGridUsagePage; diff --git a/apps/website/screens/theme-generator/components/ComponentsPreviewMap.ts b/apps/website/screens/theme-generator/components/ComponentsPreviewMap.ts index 4eff58ffb7..fa0ac7468b 100644 --- a/apps/website/screens/theme-generator/components/ComponentsPreviewMap.ts +++ b/apps/website/screens/theme-generator/components/ComponentsPreviewMap.ts @@ -33,6 +33,7 @@ import BulletedListPreview from "./previews/BulletedList"; import ParagraphPreview from "./previews/Paragraph"; import NavTabsPreview from "./previews/NavTabs"; import ContextualMenu from "./previews/ContextualMenu"; +import DataGridPreview from "./previews/DataGrid"; const SampleComponents = [ { @@ -71,6 +72,10 @@ const SampleComponents = [ name: "contextualMenu", preview: ContextualMenu, }, + { + name: "dataGrid", + preview: DataGridPreview, + }, { name: "dateInput", preview: DateInputPreview, diff --git a/apps/website/screens/theme-generator/components/previews/DataGrid.tsx b/apps/website/screens/theme-generator/components/previews/DataGrid.tsx new file mode 100644 index 0000000000..72dc444dec --- /dev/null +++ b/apps/website/screens/theme-generator/components/previews/DataGrid.tsx @@ -0,0 +1,238 @@ +import React, { useState } from "react"; +import { DxcContainer, DxcDataGrid } from "@dxc-technology/halstack-react"; +import Mode from "../Mode"; +import PreviewContainer from "./PreviewContainer"; + +type DataGridPropsType = React.ComponentProps; +type DataGridColumnsPropsType = DataGridPropsType["columns"]; +type DataGridRowsPropsType = DataGridPropsType["rows"]; + +const columns: DataGridColumnsPropsType = [ + { + key: "id", + label: "ID", + resizable: true, + sortable: true, + draggable: false, + alignment: "left", + }, + { + key: "task", + label: "Title", + resizable: true, + sortable: true, + draggable: true, + textEditable: true, + alignment: "left", + }, + { + key: "complete", + label: " % Complete", + resizable: true, + sortable: true, + draggable: true, + alignment: "right", + summaryKey: "label", + }, + { + key: "priority", + label: "Priority", + resizable: true, + draggable: true, + alignment: "center", + summaryKey: "total", + }, +]; + +const expandableRows = [ + { + id: 1, + task: "Task 1", + complete: 46, + priority: "High", + issueType: "Bug", + expandedContent: Custom content 1, + }, + { + id: 2, + task: "Task 2", + complete: 51, + priority: "High", + issueType: "Epic", + expandedContent: Custom content 2, + }, + { + id: 3, + task: "Task 3", + complete: 40, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 3, + }, + { + id: 4, + task: "Task 4", + complete: 10, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 4, + }, + { + id: 5, + task: "Task 5", + complete: 68, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 5, + }, + { + id: 6, + task: "Task 6", + complete: 37, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 6, + }, + { + id: 7, + task: "Task 7", + complete: 73, + priority: "Medium", + issueType: "Story", + expandedContent: Custom content 7, + }, + { + id: 8, + task: "Task 8", + complete: 27, + priority: "Medium", + issueType: "Story", + expandedContent: Custom content 8, + }, + { + id: 9, + task: "Task 9", + complete: 36, + priority: "Critical", + issueType: "Epic", + expandedContent: Custom content 9, + }, +]; + +const childcolumns: DataGridColumnsPropsType = [ + { + key: "name", + label: "Name", + resizable: true, + sortable: true, + alignment: "center", + }, + { + key: "value", + label: "Value", + resizable: true, + sortable: true, + draggable: true, + textEditable: true, + alignment: "right", + }, +]; + +const childRows: DataGridRowsPropsType = [ + { + name: "Root Node 1", + value: "1", + id: "a", + childRows: [ + { + name: "Child Node 1.1", + value: "1.1", + id: "aa", + childRows: [ + { + name: "Grandchild Node 1.1.1", + value: "1.1.1", + id: "aaa", + }, + { + name: "Grandchild Node 1.1.2", + value: "1.1.2", + id: "aab", + }, + ], + }, + { + name: "Child Node 1.2", + value: "1.2", + id: "ab", + }, + ], + }, + { + name: "Root Node 2", + value: "2", + id: "b", + childRows: [ + { + name: "Child Node 2.1", + value: "2.1", + id: "ba", + childRows: [ + { + name: "Grandchild Node 2.1.1", + value: "2.1.1", + id: "baa", + }, + ], + }, + { + name: "Child Node 2.2", + value: "2.2", + id: "bb", + }, + { + name: "Child Node 2.3", + value: "2.3", + id: "bc", + }, + ], + }, +] as DataGridRowsPropsType; + +const DataGridPreview = () => { + const [selectedRows, setSelectedRows] = useState((): Set => new Set()); + + return ( + + + + + + + + + + + + + + + + + + ); +}; + +export default DataGridPreview; diff --git a/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json b/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json index 76b97d5aa5..ad91a8b124 100644 --- a/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json +++ b/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json @@ -350,6 +350,43 @@ "iconColor": "color", "iconSize": "length" }, + "dataGrid": { + "rowSeparatorThickness": "length", + "rowSeparatorStyle": "bStyle", + "rowSeparatorColor": "color", + "dataBackgroundColor": "color", + "dataFontFamily": "fFamily", + "dataFontSize": "length", + "dataFontStyle": "fStyle", + "dataFontWeight": "fWeight", + "dataFontColor": "color", + "dataFontTextTransform": "fTextTransform", + "dataPaddingRight": "length", + "dataPaddingLeft": "length", + "dataRowHeight": "length", + "dataTextLineHeight": "length", + "headerBackgroundColor": "color", + "headerBorderRadius": "length", + "headerFontFamily": "fFamily", + "headerFontSize": "length", + "headerFontStyle": "fStyle", + "headerFontWeight": "fWeight", + "headerFontColor": "color", + "headerFontTextTransform": "fTextTransform", + "headerPaddingRight": "length", + "headerPaddingLeft": "length", + "headerRowHeight": "length", + "headerTextLineHeight": "length", + "headerCheckboxBackgroundColorChecked": "color", + "headerCheckboxHoverBackgroundColorChecked": "color", + "headerCheckboxBorderColor": "color", + "headerCheckboxHoverBorderColor": "color", + "headerCheckboxCheckColor": "color", + "summaryRowHeight": "length", + "focusColor": "color", + "scrollBarThumbColor": "color", + "scrollBarTrackColor": "color" + }, "dateInput": { "pickerBackgroundColor": "color", "pickerFontColor": "color", diff --git a/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json b/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json index 1d830c0186..04b6b21f80 100644 --- a/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json +++ b/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json @@ -33,6 +33,11 @@ "fontColor": "color", "iconColor": "color" }, + "dataGrid": { + "baseColor": "color", + "headerFontColor": "color", + "cellFontColor": "color" + }, "dateInput": { "baseColor": "color", "selectedFontColor": "color" diff --git a/package-lock.json b/package-lock.json index a88dc4ac03..0dc1466b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20505,6 +20505,18 @@ "react": "^16.3.0 || ^17.0.1 || ^18.0.0" } }, + "node_modules/react-data-grid": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-7.0.0-beta.44.tgz", + "integrity": "sha512-VTql8VPBJEf7E+zkrqYW+jaQT1/m47dxigWzPq59QESJ8LhU59kHyzIx06BUpjdZKdK/tzbWaw2yyJpKoNnt5g==", + "dependencies": { + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^18.0", + "react-dom": "^18.0" + } + }, "node_modules/react-docgen": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.0.3.tgz", @@ -25466,6 +25478,7 @@ "@radix-ui/react-tooltip": "^1.1.0", "color": "^4.2.3", "dayjs": "^1.11.11", + "react-data-grid": "^7.0.0-beta.44", "slugify": "^1.6.6" }, "devDependencies": { diff --git a/packages/lib/package.json b/packages/lib/package.json index 2e4521239b..fc1a08c972 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-tooltip": "^1.1.0", "color": "^4.2.3", "dayjs": "^1.11.11", + "react-data-grid": "^7.0.0-beta.44", "slugify": "^1.6.6" }, "devDependencies": { diff --git a/packages/lib/src/HalstackContext.tsx b/packages/lib/src/HalstackContext.tsx index 544515122a..8c6e669e6c 100644 --- a/packages/lib/src/HalstackContext.tsx +++ b/packages/lib/src/HalstackContext.tsx @@ -137,6 +137,12 @@ const parseTheme = (theme: DeepPartial): AdvancedTheme => { contextualMenu.sectionTitleFontColor = theme?.contextualMenu?.fontColor ?? contextualMenu.sectionTitleFontColor; contextualMenu.iconColor = theme?.contextualMenu?.iconColor ?? contextualMenu.iconColor; + const dataGridTokens = componentTokensCopy.dataGrid; + dataGridTokens.headerBackgroundColor = theme?.dataGrid?.baseColor ?? dataGridTokens.headerBackgroundColor; + dataGridTokens.headerFontColor = theme?.dataGrid?.headerFontColor ?? dataGridTokens.headerFontColor; + dataGridTokens.dataFontColor = theme?.dataGrid?.cellFontColor ?? dataGridTokens.dataFontColor; + dataGridTokens.headerCheckboxCheckColor = theme?.dataGrid?.baseColor ?? dataGridTokens.headerCheckboxCheckColor; + const dateTokens = componentTokensCopy.dateInput; dateTokens.pickerSelectedBackgroundColor = theme?.dateInput?.baseColor ?? dateTokens.pickerSelectedBackgroundColor; dateTokens.pickerSelectedFontColor = theme?.dateInput?.selectedFontColor ?? dateTokens.pickerSelectedFontColor; diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index f57b4e6b2a..cd81bf93f9 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -352,6 +352,43 @@ export const componentTokens = { iconColor: CoreTokens.color_grey_900, iconSize: "16px", }, + dataGrid: { + rowSeparatorThickness: "1px", + rowSeparatorStyle: CoreTokens.border_solid, + rowSeparatorColor: CoreTokens.color_grey_300, + dataBackgroundColor: CoreTokens.color_white, + dataFontFamily: CoreTokens.type_sans, + dataFontSize: CoreTokens.type_scale_02, + dataFontStyle: CoreTokens.type_normal, + dataFontWeight: CoreTokens.type_regular, + dataFontColor: CoreTokens.color_black, + dataFontTextTransform: "none", + dataPaddingRight: CoreTokens.spacing_8, + dataPaddingLeft: CoreTokens.spacing_8, + dataRowHeight: 36, + dataTextLineHeight: "normal", + headerBackgroundColor: CoreTokens.color_purple_700, + headerBorderRadius: "4px", + headerFontFamily: CoreTokens.type_sans, + headerFontSize: CoreTokens.type_scale_02, + headerFontStyle: CoreTokens.type_normal, + headerFontWeight: CoreTokens.type_bold, + headerFontColor: CoreTokens.color_white, + headerFontTextTransform: "none", + headerPaddingRight: CoreTokens.spacing_8, + headerPaddingLeft: CoreTokens.spacing_8, + headerRowHeight: 36, + headerTextLineHeight: "normal", + headerCheckboxBackgroundColorChecked: CoreTokens.color_white, + headerCheckboxHoverBackgroundColorChecked: CoreTokens.color_grey_200, + headerCheckboxBorderColor: CoreTokens.color_white, + headerCheckboxHoverBorderColor: CoreTokens.color_white, + headerCheckboxCheckColor: CoreTokens.color_purple_700, + summaryRowHeight: 36, + focusColor: CoreTokens.color_blue_600, + scrollBarThumbColor: CoreTokens.color_grey_700, + scrollBarTrackColor: CoreTokens.color_grey_300, + }, dateInput: { pickerBackgroundColor: CoreTokens.color_white, pickerFontColor: CoreTokens.color_black, @@ -1321,6 +1358,11 @@ export type OpinionatedTheme = { fontColor: string; iconColor: string; }; + dataGrid: { + baseColor: string; + headerFontColor: string; + cellFontColor: string; + }; dateInput: { baseColor: string; selectedFontColor: string; diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx new file mode 100644 index 0000000000..465b89568c --- /dev/null +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -0,0 +1,341 @@ +import Title from "../../.storybook/components/Title"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import DxcDataGrid from "./DataGrid"; +import DxcContainer from "../container/Container"; +import { GridColumn, HierarchyGridRow } from "./types"; +import { useState } from "react"; +import { disabledRules } from "../../test/accessibility/rules/specific/data-grid/disabledRules"; +import preview from "../../.storybook/preview"; +import { userEvent, within } from "@storybook/test"; + +export default { + title: "Data Grid", + component: DxcDataGrid, + parameters: { + a11y: { + config: { + rules: [ + ...disabledRules.map((ruleId) => ({ id: ruleId, reviewOnFail: true })), + ...preview?.parameters?.a11y?.config?.rules, + ], + }, + }, + }, +}; + +const columns: GridColumn[] = [ + { + key: "id", + label: "ID", + resizable: true, + sortable: true, + draggable: false, + alignment: "left", + }, + { + key: "task", + label: "Title", + resizable: true, + sortable: true, + draggable: true, + textEditable: true, + alignment: "left", + }, + { + key: "complete", + label: " % Complete", + resizable: true, + sortable: true, + draggable: true, + alignment: "right", + summaryKey: "label", + }, + { + key: "priority", + label: "Priority", + resizable: true, + draggable: true, + alignment: "center", + summaryKey: "total", + }, +]; + +const expandableRows = [ + { + id: 1, + task: "Task 1", + complete: 46, + priority: "High", + issueType: "Bug", + expandedContent: Custom content 1, + expandedContentHeight: 470, + }, + { + id: 2, + task: "Task 2", + complete: 51, + priority: "High", + issueType: "Epic", + expandedContent: Custom content 1, + }, + { + id: 3, + task: "Task 3", + complete: 40, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 1, + }, + { + id: 4, + task: "Task 4", + complete: 10, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 1, + }, + { + id: 5, + task: "Task 5", + complete: 68, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 1, + }, + { + id: 6, + task: "Task 6", + complete: 37, + priority: "High", + issueType: "Improvement", + expandedContent: Custom content 1, + }, + { + id: 7, + task: "Task 7", + complete: 73, + priority: "Medium", + issueType: "Story", + expandedContent: Custom content 1, + }, + { + id: 8, + task: "Task 8", + complete: 27, + priority: "Medium", + issueType: "Story", + expandedContent: Custom content 1, + }, + { + id: 9, + task: "Task 9", + complete: 36, + priority: "Critical", + issueType: "Epic", + expandedContent: Custom content 1, + }, +]; + +const childcolumns: GridColumn[] = [ + { + key: "name", + label: "Name", + resizable: true, + sortable: true, + alignment: "center", + }, + { + key: "value", + label: "Value", + resizable: true, + sortable: true, + draggable: true, + textEditable: true, + alignment: "right", + }, +]; + +const childRows: HierarchyGridRow[] = [ + { + name: "Root Node 1", + value: "1", + id: "a", + childRows: [ + { + name: "Child Node 1.1", + value: "1.1", + id: "aa", + childRows: [ + { + name: "Grandchild Node 1.1.1", + value: "1.1.1", + id: "aaa", + }, + { + name: "Grandchild Node 1.1.2", + value: "1.1.2", + id: "aab", + }, + ], + }, + { + name: "Child Node 1.2", + value: "1.2", + id: "ab", + }, + ], + }, + { + name: "Root Node 2", + value: "2", + id: "b", + childRows: [ + { + name: "Child Node 2.1", + value: "2.1", + id: "ba", + childRows: [ + { + name: "Grandchild Node 2.1.1", + value: "2.1.1", + id: "baa", + }, + ], + }, + { + name: "Child Node 2.2", + value: "2.2", + id: "bb", + }, + { + name: "Child Node 2.3", + value: "2.3", + id: "bc", + }, + ], + }, +] as HierarchyGridRow[]; + +export const Chromatic = () => { + const [selectedRows, setSelectedRows] = useState((): Set => new Set()); + const [selectedChildRows, setSelectedChildRows] = useState((): Set => new Set()); + return ( + <> + + + <DxcDataGrid columns={columns} rows={expandableRows} uniqueRowId="id" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Expandable" theme="light" level={4} /> + <DxcDataGrid columns={columns} rows={expandableRows} uniqueRowId="id" expandable /> + </ExampleContainer> + <ExampleContainer> + <Title title="Selectable" theme="light" level={4} /> + <DxcDataGrid + columns={columns} + rows={expandableRows} + uniqueRowId="task" + selectable + selectedRows={selectedRows} + onSelectRows={setSelectedRows} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Selectable & expandable" theme="light" level={4} /> + <DxcDataGrid + columns={columns} + rows={expandableRows} + uniqueRowId="task" + expandable + selectable + selectedRows={selectedRows} + onSelectRows={setSelectedRows} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="DataGrid with children" theme="light" level={4} /> + <DxcDataGrid columns={childcolumns} rows={childRows} uniqueRowId="id" /> + </ExampleContainer> + <ExampleContainer> + <Title title="DataGrid with children" theme="light" level={4} /> + <DxcDataGrid + columns={childcolumns} + rows={childRows} + uniqueRowId="value" + selectable + selectedRows={selectedChildRows} + onSelectRows={setSelectedChildRows} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Summary row" theme="light" level={4} /> + <DxcDataGrid + columns={columns} + rows={expandableRows} + summaryRow={{ label: "Total", total: 100 }} + uniqueRowId="id" + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Scrollable Data Grid" theme="light" level={4} /> + <DxcContainer height="250px"> + <DxcDataGrid columns={columns} rows={expandableRows} uniqueRowId="id" /> + </DxcContainer> + </ExampleContainer> + </> + ); +}; + +const DataGridSortedChildren = () => { + const [selectedChildRows, setSelectedChildRows] = useState((): Set<number | string> => new Set()); + return ( + <ExampleContainer> + <DxcDataGrid + columns={childcolumns} + rows={childRows} + uniqueRowId="id" + selectable + onSelectRows={setSelectedChildRows} + selectedRows={selectedChildRows} + /> + </ExampleContainer> + ); +}; + +export const DataGridSortedWithChildren = DataGridSortedChildren.bind({}); +DataGridSortedWithChildren.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getAllByRole("checkbox")[0]); + await userEvent.click(canvas.getByText("Root Node 1")); + await userEvent.click(canvas.getByText("Root Node 2")); + await userEvent.click(canvas.getByText("Child Node 1.1")); + await userEvent.click(canvas.getByText("Child Node 2.1")); + await userEvent.click(canvas.getAllByRole("columnheader")[1]); + await userEvent.click(canvas.getAllByRole("columnheader")[1]); + await userEvent.click(canvas.getAllByRole("checkbox")[5]); +}; + +const DataGridSortedExpandable = () => { + const [selectedRows, setSelectedRows] = useState((): Set<number | string> => new Set()); + return ( + <ExampleContainer> + <DxcDataGrid + columns={columns} + rows={expandableRows} + uniqueRowId="task" + expandable + selectable + onSelectRows={setSelectedRows} + selectedRows={selectedRows} + /> + </ExampleContainer> + ); +}; + +export const DataGridSortedExpanded = DataGridSortedExpandable.bind({}); +DataGridSortedExpanded.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getAllByRole("button")[0]); + await userEvent.click(canvas.getAllByRole("button")[1]); + await userEvent.click(canvas.getAllByRole("columnheader")[4]); +}; diff --git a/packages/lib/src/data-grid/DataGrid.test.tsx b/packages/lib/src/data-grid/DataGrid.test.tsx new file mode 100644 index 0000000000..70fef67bac --- /dev/null +++ b/packages/lib/src/data-grid/DataGrid.test.tsx @@ -0,0 +1,116 @@ +import { findAllByRole, fireEvent, getAllByRole, render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import DxcDataGrid from "./DataGrid"; +import { GridColumn, HierarchyGridRow } from "./types"; + +const columns: GridColumn[] = [ + { + key: "id", + label: "ID", + resizable: true, + sortable: true, + draggable: false, + alignment: "left", + }, + { + key: "complete", + label: " % Complete", + resizable: true, + sortable: true, + draggable: true, + }, +]; + +const expandableRows = [ + { + id: 1, + complete: 46, + expandedContent: <div> Custom content 1</div>, + }, + { + id: 2, + complete: 51, + expandedContent: <div> Custom content 2</div>, + }, + { + id: 3, + complete: 40, + expandedContent: <div> Custom content 3</div>, + }, + { + id: 4, + complete: 10, + expandedContent: <div> Custom content 4</div>, + }, + { + id: 5, + complete: 1, + expandedContent: <div> Custom content 5</div>, + }, +]; + +const rowsWithChildren: HierarchyGridRow[] = [ + { + id: 1, + complete: 46, + childRows: [ + { + id: 1.1, + complete: 46, + childRows: [ + { + id: "1.1a", + complete: 46, + }, + ], + }, + { + complete: 46, + value: 1.2, + }, + ], + }, + { + id: 2, + complete: 51, + }, + { + id: 3, + complete: 40, + }, + { + id: 4, + complete: 10, + }, + { + id: 5, + complete: 1, + }, +] as HierarchyGridRow[]; + +describe("Data grid component tests", () => { + beforeAll(() => { + global.CSS = { + escape: (str) => str, + }; + window.HTMLElement.prototype.scrollIntoView = jest.fn; + }); + test("Renders with correct content", async () => { + const { getByText, getAllByRole } = await render(<DxcDataGrid columns={columns} rows={expandableRows} />); + expect(getByText("46")).toBeTruthy(); + const rows = getAllByRole("row"); + expect(rows.length).toBe(5); + }); + // test("Content is sorted correctly", async () => { + // const { getByText, getAllByRole } = await render(<DxcDataGrid columns={columns} rows={expandableRows} />); + // expect(getByText("% Complete")).toBeTruthy(); + // const headerCell = screen.getAllByRole("columnheader")[1]; + // expect(getAllByRole("gridcell")[0].textContent).toBe("1"); + // expect(headerCell.textContent).toBe(" % Complete"); + // await fireEvent.click(headerCell); + // expect(headerCell.getAttribute("aria-sort")).toBe("ascending"); + // expect(getByText("5")).toBeTruthy(); + // // await waitFor(() => expect(getAllByRole("gridcell")[0].textContent).toBe("4")); + // //waitFor(() => expect(getAllByRole("gridcell").length).toBe(8)); + // }); +}); diff --git a/packages/lib/src/data-grid/DataGrid.tsx b/packages/lib/src/data-grid/DataGrid.tsx new file mode 100644 index 0000000000..b7c90e1f2f --- /dev/null +++ b/packages/lib/src/data-grid/DataGrid.tsx @@ -0,0 +1,348 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useMemo, useState } from "react"; +import DataGridPropsType, { HierarchyGridRow, GridRow, ExpandableGridRow } from "./types"; +import DataGrid, { SortColumn } from "react-data-grid"; +import "react-data-grid/lib/styles.css"; + +import { + convertToRDGColumns, + rowKeyGetter, + sortRows, + renderSortStatus, + addRow, + renderCheckbox, + renderHeaderCheckbox, + sortHierarchyRows, + renderHierarchyTrigger, + renderExpandableTrigger, +} from "./utils"; +import styled, { ThemeProvider } from "styled-components"; +import useTheme from "../useTheme"; + +const DxcDataGrid = ({ + columns, + rows, + selectable, + expandable, + onSelectRows, + selectedRows, + uniqueRowId, + summaryRow, + onGridRowsChange, +}: DataGridPropsType): JSX.Element => { + const [rowsToRender, setRowsToRender] = useState<GridRow[] | HierarchyGridRow[] | ExpandableGridRow[]>(rows); + const colorsTheme = useTheme(); + // Proccess columns prop into usable columns based on other props + const columnsToRender = useMemo(() => { + let expectedColumns = columns + // .filter((column) => visibleColumns.includes(column.name)) + .map((column) => { + // if (!visibleColumns.includes(column.name)) + // return { + // key: column.key, + // name: column.name, + // colSpan() { + // return 0; + // }, + // }; + + return convertToRDGColumns(column, summaryRow); + }); + if (expandable) { + expectedColumns = [ + { + key: "expanded", + name: "", + maxWidth: 36, + width: 36, + minWidth: 36, + colSpan(args) { + return args.type === "ROW" && args.row.isExpandedChildContent ? columns.length + 1 : undefined; + }, + renderCell({ row }) { + if (row.isExpandedChildContent) { + // if it is expanded content + return row.expandedChildContent || <></>; + } + // if row has expandable content + return ( + <ActionContainer id="action"> + {row.expandedContent && renderExpandableTrigger(row, rowsToRender, uniqueRowId, setRowsToRender)} + </ActionContainer> + ); + }, + }, + ...expectedColumns, + ]; + } + if (!expandable && rows.some((row) => Array.isArray(row.childRows) && row.childRows.length > 0) && uniqueRowId) { + // only the first column will be clickable and will expand the rows + const firstColumnKey = expectedColumns[0].key; + expectedColumns[0] = { + ...expectedColumns[0], + renderCell({ row }) { + if (row.childRows?.length) { + return ( + <HierarchyContainer level={row.rowLevel || 0}> + {renderHierarchyTrigger(rowsToRender, row, uniqueRowId, firstColumnKey, setRowsToRender)} + </HierarchyContainer> + ); + } + return ( + <HierarchyContainer level={row.rowLevel || 0} className="ellipsis-cell"> + {row[firstColumnKey]} + </HierarchyContainer> + ); + }, + }; + } + if (selectable) { + expectedColumns = [ + { + key: "selected", + name: "", + maxWidth: 36, + width: 36, + minWidth: 36, + renderCell({ row }) { + if (!row.isExpandedChildContent) { + return ( + <ActionContainer id="action"> + {renderCheckbox(rows, row, uniqueRowId, selectedRows, onSelectRows)} + </ActionContainer> + ); + } + }, + renderHeaderCell: () => { + return ( + <ActionContainer id="action"> + {renderHeaderCheckbox(rows, uniqueRowId, selectedRows, colorsTheme, onSelectRows)} + </ActionContainer> + ); + }, + }, + ...expectedColumns, + ]; + } + return expectedColumns; + }, [selectable, expandable, columns, rowsToRender, onSelectRows, rows, summaryRow, uniqueRowId, selectedRows]); + // array with the order of the columns + const [columnsOrder, setColumnsOrder] = useState((): number[] => columnsToRender.map((_, index) => index)); + const [sortColumns, setSortColumns] = useState<readonly SortColumn[]>([]); + // const [visibleColumns, setVisibleColumns] = useState( + // columnsNamesIntoOptions(columns).map((visibleColumn) => { + // return visibleColumn.value; + // }) + // ); + + useEffect(() => { + setColumnsOrder(columnsToRender.map((_, index) => index)); + }, [columnsToRender]); + + const reorderedColumns = useMemo(() => { + // Array ordered by columnsOrder + return columnsOrder.map((index) => columnsToRender[index]); + }, [columnsOrder, columnsToRender]); + + const onColumnsReorder = (sourceKey: string, targetKey: string) => { + setColumnsOrder((columnsOrder) => { + const sourceColumnOrderIndex = columnsOrder.findIndex((index) => columnsToRender[index].key === sourceKey); + const targetColumnOrderIndex = columnsOrder.findIndex((index) => columnsToRender[index].key === targetKey); + const newColumnsOrder = columnsOrder.slice(); + newColumnsOrder.splice(sourceColumnOrderIndex, 1); + newColumnsOrder.splice(targetColumnOrderIndex, 0, columnsOrder[sourceColumnOrderIndex]); + return newColumnsOrder; + }); + }; + + const onRowsChange = (newRows: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[]) => { + // call function to change rows, like when they have been edited + onGridRowsChange(newRows); + }; + + const sortedRows = useMemo((): readonly GridRow[] => { + if (expandable && sortColumns.length >= 0) { + const sortedRows = sortRows( + rowsToRender.filter((row) => !row.isExpandedChildContent), + sortColumns + ); + rowsToRender + .filter((row) => row.isExpandedChildContent) + .map((expandedRow) => + addRow( + sortedRows, + sortedRows.findIndex((trigger) => rowKeyGetter(trigger, uniqueRowId) === expandedRow.triggerRowKey) + 1, + expandedRow + ) + ); + return sortedRows; + } else if (!expandable && sortColumns.length >= 0) { + if (uniqueRowId) return sortHierarchyRows(rowsToRender, sortColumns, uniqueRowId); + } + return rowsToRender; + }, [expandable, rowsToRender, sortColumns, uniqueRowId]); + + return ( + <ThemeProvider theme={colorsTheme.dataGrid}> + <DataGridContainer> + {/* {columnsVisibilityFilter && ( + <DxcSelect + multiple + options={columnsNamesIntoOptions(columns)} + defaultValue={visibleColumns} + onChange={(event) => setVisibleColumns(event.value)} + /> + )} */} + <DataGrid + columns={reorderedColumns} + rows={sortedRows} + onColumnsReorder={onColumnsReorder} + onRowsChange={onRowsChange} + renderers={{ renderSortStatus }} + sortColumns={sortColumns} + onSortColumnsChange={setSortColumns} + rowKeyGetter={(row) => uniqueRowId && rowKeyGetter(row, uniqueRowId)} + rowHeight={(row) => { + if ( + row.isExpandedChildContent && + typeof row.expandedContentHeight === "number" && + row.expandedContentHeight > 0 + ) { + return row.expandedContentHeight; + } + return colorsTheme.dataGrid.dataRowHeight; + }} + selectedRows={selectedRows} + bottomSummaryRows={summaryRow ? [summaryRow] : undefined} + headerRowHeight={colorsTheme.dataGrid.headerRowHeight} + summaryRowHeight={colorsTheme.dataGrid.summaryRowHeight} + className="fill-grid" + /> + </DataGridContainer> + </ThemeProvider> + ); +}; + +const ActionContainer = styled.div` + display: flex; + height: 100%; + align-items: center; + justify-content: center; + font-size: 14px; + width: 100%; +`; + +const HierarchyContainer = styled.div<{ + level: number; +}>` + padding-left: ${(props) => `calc(${props.theme.dataPaddingLeft} * ${props.level})`}; + button { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: 0.5rem; + padding: 0px; + border: 0px; + width: 100%; + height: ${(props) => props.theme.dataRowHeight}px; + background: transparent; + text-align: left; + font-size: ${(props) => props.theme.dataFontSize}; + font-family: inherit; + color: inherit; + cursor: pointer; + } +`; + +const DataGridContainer = styled.div` + width: 100%; + height: 100%; + .rdg { + border-radius: 4px; + height: 100%; + border: 0px; + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + &::-webkit-scrollbar-thumb { + background-color: ${(props) => props.theme.scrollBarThumbColor}; + border-radius: 6px; + } + &::-webkit-scrollbar-track { + background-color: ${(props) => props.theme.scrollBarTrackColor}; + border-radius: 6px; + } + } + .rdg-cell:has(> #action) { + padding: 0px; + } + .rdg-cell { + display: grid; + align-items: center; + width: 100%; + padding: 0px ${(props) => props.theme.dataPaddingRight} 0 ${(props) => props.theme.dataPaddingLeft}; + font-family: ${(props) => props.theme.dataFontFamily}; + font-size: ${(props) => props.theme.dataFontSize}; + font-style: ${(props) => props.theme.dataFontStyle}; + font-weight: ${(props) => props.theme.dataFontWeight}; + color: ${(props) => props.theme.dataFontColor}; + text-transform: ${(props) => props.theme.dataFontTextTransform}; + line-height: ${(props) => props.theme.dataTextLineHeight}; + border-bottom: ${(props) => + `${props.theme.rowSeparatorThickness} ${props.theme.rowSeparatorStyle} ${props.theme.rowSeparatorColor}`}; + border-right: ${(props) => + `${props.theme.rowSeparatorThickness} ${props.theme.rowSeparatorStyle} ${props.theme.rowSeparatorColor}`}; + background-color: ${(props) => props.theme.dataBackgroundColor}; + } + .rdg-header-row { + border-top-left-radius: ${(props) => props.theme.headerBorderRadius}; + border-top-right-radius: ${(props) => props.theme.headerBorderRadius}; + .rdg-cell { + font-family: ${(props) => props.theme.headerFontFamily}; + font-size: ${(props) => props.theme.headerFontSize}; + font-style: ${(props) => props.theme.headerFontStyle}; + font-weight: ${(props) => props.theme.headerFontWeight}; + color: ${(props) => props.theme.headerFontColor}; + text-transform: ${(props) => props.theme.headerFontTextTransform}; + padding: 0px ${(props) => props.theme.headerPaddingRight} 0 ${(props) => props.theme.headerPaddingLeft}; + line-height: ${(props) => props.theme.headerTextLineHeight}; + background-color: ${(props) => props.theme.headerBackgroundColor}; + .sortIconContainer { + margin-left: 0.5rem; + display: flex; + height: 100%; + align-items: center; + } + } + } + .rdg-row { + .rdg-cell:last-child { + border-right: 0px; + } + } + .rdg-summary-row { + background-color: #fafafa; + .rdg-cell { + border: 0px; + font-weight: 600; + } + } + .ellipsis-cell { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + } + .align-left { + text-align: left; + } + .align-center { + text-align: center; + } + .align-right { + text-align: right; + } +`; + +export default DxcDataGrid; diff --git a/packages/lib/src/data-grid/types.ts b/packages/lib/src/data-grid/types.ts new file mode 100644 index 0000000000..e723c0bde1 --- /dev/null +++ b/packages/lib/src/data-grid/types.ts @@ -0,0 +1,69 @@ +export type GridColumn = { + key: string; + label: string; + resizable?: boolean; + sortable?: boolean; + draggable?: boolean; + textEditable?: boolean; + summaryKey?: string; + alignment?: "left" | "right" | "center"; +}; + +export type GridRow = { + [key: string]: React.ReactNode | undefined; +}; + +export type HierarchyGridRow = GridRow & { + childRows?: HierarchyGridRow[] | GridRow[]; +}; + +export type ExpandableGridRow = GridRow & { + expandedContent?: React.ReactNode; + expandedContentHeight?: number; +}; + +export type ExpandableRows = { + rows: ExpandableGridRow[]; + expandable: true; + uniqueRowId: string; +}; + +export type HierarchyRows = { + rows: HierarchyGridRow[]; + uniqueRowId: string; + expandable?: false; +}; + +export type SelectableGridProps = + | { + selectable: true; + selectedRows: Set<string | number>; + onSelectRows: (selectedRows: Set<number | string>) => void; + uniqueRowId: string; + } + | { + selectable?: false; + selectedRows?: never; + onSelectRows?: never; + uniqueRowId?: string; + }; + +export type CommonProps = { + columns: GridColumn[]; + summaryRow?: GridRow; + onGridRowsChange?: (rows: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[]) => void; +}; + +export type BasicGridProps = { + rows: GridRow[]; + expandable?: false; +}; + +type Props = CommonProps & + ( + | (BasicGridProps & SelectableGridProps) + | (ExpandableRows & SelectableGridProps) + | (HierarchyRows & SelectableGridProps) + ); + +export default Props; diff --git a/packages/lib/src/data-grid/utils.tsx b/packages/lib/src/data-grid/utils.tsx new file mode 100644 index 0000000000..bf22ed6d2c --- /dev/null +++ b/packages/lib/src/data-grid/utils.tsx @@ -0,0 +1,414 @@ +import DxcActionIcon from "../action-icon/ActionIcon"; +import DxcCheckbox from "../checkbox/Checkbox"; +import { AdvancedTheme } from "../common/variables"; +import { DeepPartial, HalstackProvider } from "../HalstackContext"; +import DxcIcon from "../icon/Icon"; +import { GridColumn, HierarchyGridRow, GridRow, ExpandableGridRow } from "./types"; +import { Column, RenderSortStatusProps, SortColumn, textEditor } from "react-data-grid"; + +const overwriteTheme = (theme: DeepPartial<AdvancedTheme>) => { + const newTheme = { + checkbox: { + backgroundColorChecked: theme.dataGrid.headerCheckboxBackgroundColorChecked, + hoverBackgroundColorChecked: theme.dataGrid.headerCheckboxHoverBackgroundColorChecked, + borderColor: theme.dataGrid.headerCheckboxBorderColor, + hoverBorderColor: theme.dataGrid.headerCheckboxHoverBorderColor, + checkColor: theme.dataGrid.headerCheckboxCheckColor, + focusColor: theme.dataGrid.focusColor, + }, + }; + + return newTheme; +}; + +// Column<any, any> type added to avoid conflicts with SelectColumn typing from RDG +export const convertToRDGColumns = (gridColumn: GridColumn, summaryRow?: GridRow): Column<any, any> => { + return { + key: gridColumn.key, + name: gridColumn.label, + resizable: gridColumn.resizable, + sortable: gridColumn.sortable, + draggable: gridColumn.draggable, + editable: gridColumn.textEditable, + headerCellClass: gridColumn.alignment ? `align-${gridColumn.alignment}` : `align-left`, + renderEditCell: gridColumn.textEditable ? textEditor : undefined, + renderCell: ({ row }) => { + return ( + <div className={`ellipsis-cell ${gridColumn.alignment ? "align-" + gridColumn.alignment : "align-left"}`}> + {row[gridColumn.key]} + </div> + ); + }, + renderSummaryCell: () => { + return gridColumn.summaryKey ? ( + <div className={`ellipsis-cell ${gridColumn.alignment ? "align-" + gridColumn.alignment : "align-left"}`}> + {summaryRow?.[gridColumn.summaryKey]} + </div> + ) : undefined; + }, + }; +}; + +export const renderSortStatus = ({ sortDirection }: RenderSortStatusProps) => { + return ( + <div className="sortIconContainer"> + {sortDirection !== undefined ? ( + sortDirection === "ASC" ? ( + <DxcIcon icon="Keyboard_Arrow_Up" /> + ) : ( + <DxcIcon icon="Keyboard_Arrow_Down" /> + ) + ) : ( + <DxcIcon icon="Expand_All" /> + )} + </div> + ); +}; + +export const renderExpandableTrigger = ( + row: ExpandableGridRow, + rows: ExpandableGridRow[], + uniqueRowId: string, + setRowsToRender: (value: React.SetStateAction<ExpandableGridRow[] | GridRow[] | HierarchyGridRow[]>) => void +) => { + return ( + <DxcActionIcon + icon={row.contentIsExpanded ? "arrow_drop_down" : "arrow_right"} + title="Expand content" + aria-expanded={row.contentIsExpanded} + onClick={() => { + row.contentIsExpanded = !row.contentIsExpanded; + if (row.contentIsExpanded) { + const rowIndex = rows.findIndex((rowToRender) => row === rowToRender); + setRowsToRender((rows) => { + const newRows = [...rows]; + addRow(newRows, rowIndex + 1, { + isExpandedChildContent: row.contentIsExpanded, + [uniqueRowId]: rowKeyGetter(row, uniqueRowId) + "_expanded", + expandedChildContent: row.expandedContent, + triggerRowKey: rowKeyGetter(row, uniqueRowId), + expandedContentHeight: row.expandedContentHeight, + }); + return newRows; + }); + } else { + const rowIndex = rows.findIndex((rowToRender) => row === rowToRender); + setRowsToRender((rows) => { + const newRows = [...rows]; + deleteRow(newRows, rowIndex + 1); + return newRows; + }); + } + }} + /> + ); +}; + +export const renderHierarchyTrigger = ( + rows: HierarchyGridRow[], + triggerRow: HierarchyGridRow, + uniqueRowId: string, + columnKey: string, + setRowsToRender: (value: React.SetStateAction<GridRow[] | ExpandableGridRow[] | HierarchyGridRow[]>) => void +) => { + return ( + <button + onClick={() => { + let newRowsToRender = [...rows]; + if (!triggerRow.visibleChildren) { + const rowIndex = rows.findIndex((rowToRender) => triggerRow === rowToRender); + triggerRow.childRows?.map((childRow: HierarchyGridRow, index: number) => { + childRow.rowLevel = + triggerRow.rowLevel && typeof triggerRow.rowLevel === "number" ? triggerRow.rowLevel + 1 : 1; + childRow.parentKey = rowKeyGetter(triggerRow, uniqueRowId); + addRow(newRowsToRender, rowIndex + 1 + index, childRow); + }); + } else { + // The children of the row that is being collapsed are added to an array + const rowsToRemove: HierarchyGridRow[] = [ + ...rows.filter( + (rowToRender) => rowToRender.parentKey && rowToRender.parentKey === rowKeyGetter(triggerRow, uniqueRowId) + ), + ]; + // The children are checked if any of them has any other children of their own + const rowsToCheck = [...rowsToRemove]; + while (rowsToCheck.length > 0) { + const currentRow = rowsToCheck.pop(); + const childRows = currentRow?.visibleChildren && currentRow?.childRows ? currentRow.childRows : []; + + rowsToRemove.push(...childRows); + rowsToCheck.push(...childRows); + } + newRowsToRender = rows.filter( + (row) => + !rowsToRemove + .map((rowToRemove) => { + if (rowToRemove.visibleChildren) { + rowToRemove.visibleChildren = false; + } + return rowKeyGetter(rowToRemove, uniqueRowId); + }) + .includes(rowKeyGetter(row, uniqueRowId)) + ); + } + triggerRow.visibleChildren = !triggerRow.visibleChildren; + setRowsToRender(newRowsToRender); + }} + > + <DxcIcon icon={triggerRow.visibleChildren ? "Keyboard_Arrow_Down" : "Chevron_Right"} /> + <span className="ellipsis-cell">{triggerRow[columnKey]}</span> + </button> + ); +}; + +export const renderCheckbox = ( + rows: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[], + row: GridRow | HierarchyGridRow | ExpandableGridRow, + uniqueRowId: string, + selectedRows: Set<number | string>, + onSelectRows: (selectedRows: Set<number | string>) => void +) => { + return ( + <DxcCheckbox + checked={selectedRows.has(rowKeyGetter(row, uniqueRowId))} + onChange={(checked) => { + const selected = new Set(selectedRows); + checked ? selected.add(rowKeyGetter(row, uniqueRowId)) : selected.delete(rowKeyGetter(row, uniqueRowId)); + if (row.childRows && Array.isArray(row.childRows)) { + getChildrenSelection(row.childRows, uniqueRowId, selected, checked); + } + if (row.parentKey) + getParentSelectedState(rows, rowKeyGetter(row, uniqueRowId), row.parentKey, uniqueRowId, selected, checked); + onSelectRows(selected); + }} + /> + ); +}; + +/** + * Render the specific checkbox in the header + * @param rows + * @param uniqueRowId + * @param selectedRows + * @param onSelectRows + */ +export const renderHeaderCheckbox = ( + rows: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[], + uniqueRowId: string, + selectedRows: Set<number | string>, + colorsTheme: DeepPartial<AdvancedTheme>, + onSelectRows: (selected: Set<number | string>) => void +) => { + return ( + <HalstackProvider advancedTheme={overwriteTheme(colorsTheme)}> + <DxcCheckbox + checked={!rows.some((row) => !selectedRows.has(rowKeyGetter(row, uniqueRowId)))} + onChange={(checked) => { + const selected = new Set<number | string>(); + if (checked) { + rows.map((row) => { + selected.add(rowKeyGetter(row, uniqueRowId)); + if (row.childRows && Array.isArray(row.childRows)) + getChildrenSelection(row.childRows, uniqueRowId, selected, checked); + }); + } + onSelectRows(selected); + }} + /> + </HalstackProvider> + ); +}; + +export const rowKeyGetter = (row: any, uniqueRowId: string) => { + return row[uniqueRowId]; +}; + +export const sortRows = (rows: GridRow[], sortColumns: readonly SortColumn[], reversed?: boolean) => { + return [...rows].sort((a, b) => { + for (const sort of sortColumns) { + const sortValueA = a[sort.columnKey]; + const sortValueB = b[sort.columnKey]; + let compResult = 0; + + if (sortValueA && sortValueB) { + compResult = sortValueA > sortValueB ? 1 : sortValueA < sortValueB ? -1 : 0; + } + + if (compResult !== 0) { + if (reversed) return sort.direction === "ASC" ? -compResult : compResult; + return sort.direction === "ASC" ? compResult : -compResult; + } + } + return 0; + }); +}; + +export const sortHierarchyRows = ( + rows: HierarchyGridRow[], + sortColumns: readonly SortColumn[], + uniqueRowId: string +) => { + const parentsSorted = sortRows( + rows.filter((row) => !row.parentKey), + sortColumns + ); + // check if there are child rows + if (rows.length === parentsSorted.length) return parentsSorted; + else { + let sortedChildren = sortRows( + rows.filter((row) => row.parentKey), + sortColumns, + true + ); + // add children directly under the parent if it is available + while (sortedChildren.length) { + if (uniqueRowId) { + sortedChildren = sortedChildren.reduce( + ( + remainingChilds: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[], + child: GridRow | HierarchyGridRow | ExpandableGridRow + ) => { + const parentIndex = parentsSorted.findIndex( + (parent) => rowKeyGetter(parent, uniqueRowId) === child.parentKey + ); + if (parentIndex >= 0) { + parentsSorted.splice(parentIndex + 1, 0, child); + } else { + remainingChilds.push(child); + } + return remainingChilds; + }, + [] + ); + } + } + return parentsSorted; + } +}; + +// function columnsNamesIntoOptions(columns: GridColumn[]) { +// return columns.map((column) => { +// return { label: column.name, value: column.name }; +// }); +// } + +// export function addUniqueId( +// row: GridRow | HierarchyGridRow | ExpandableGridRow, +// counter: number | string, +// expandable?: boolean +// ) { +// row.uniqueRowId = counter; +// if (!expandable && row.childRows && Array.isArray(row.childRows)) { +// row.childRows.forEach((childRow: HierarchyGridRow, index) => +// addUniqueId(childRow, `${counter}_${index}`) +// ); +// } +// } + +export const addRow = ( + rowList: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[], + index: number, + row: GridRow | HierarchyGridRow | ExpandableGridRow +) => { + rowList.splice(index, 0, row); +}; + +export const deleteRow = (rowList: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[], index: number) => { + rowList.splice(index, 1); +}; + +/** + * @param rowList List of rows in which to look for the row, it will also look for the row in the childRows + * @param uniqueRowId The key that contains the unique value of the row + * @param uniqueRowIdValue Unique value to identify the row + * @returns A copy of the Row + */ +export const rowFinderBasedOnId = ( + rowList: GridRow[] | HierarchyGridRow[] | ExpandableGridRow[], + uniqueRowId: string, + uniqueRowIdValue: React.ReactNode +): GridRow | HierarchyGridRow | ExpandableGridRow | undefined => { + let foundRow: GridRow | HierarchyGridRow | ExpandableGridRow | undefined = undefined; + rowList.forEach((row) => { + if (rowKeyGetter(row, uniqueRowId) === uniqueRowIdValue) { + foundRow = { ...row }; + } + if (row.childRows && Array.isArray(row.childRows) && !foundRow) { + foundRow = rowFinderBasedOnId(row.childRows, uniqueRowId, uniqueRowIdValue); + } + }); + if (foundRow) return foundRow; +}; + +export const getChildrenSelection = ( + rowList: HierarchyGridRow[], + uniqueRowId: string, + selectedRows: Set<number | string>, + checked: boolean +) => { + rowList.forEach((row) => { + if (row.childRows) { + getChildrenSelection(row.childRows, uniqueRowId, selectedRows, checked); + } + if (checked) selectedRows.add(rowKeyGetter(row, uniqueRowId)); + else { + selectedRows.delete(rowKeyGetter(row, uniqueRowId)); + } + }); +}; + +/** + * Check if the parent and its parent should be selected/unselected + * @param rowList + * @param uniqueRowKeyValue Unique value of the selected row + * @param parentKeyValue Unique value of the parent Row + * @param uniqueRowId Key where the unique value is located + * @param changedRows + * @param checkedStateToMatch + */ +export const getParentSelectedState = ( + rowList: HierarchyGridRow[], + uniqueRowKeyValue: React.ReactNode, + parentKeyValue: React.ReactNode, + uniqueRowId: string, + selectedRows: Set<number | string>, + checkedStateToMatch: boolean +) => { + const parentRow = rowFinderBasedOnId(rowList, uniqueRowId, parentKeyValue); + // we are unselecting or any of the other childRows is unselected + if ( + !checkedStateToMatch || + (parentRow?.childRows && + Array.isArray(parentRow.childRows) && + parentRow.childRows + .filter((row) => rowKeyGetter(row, uniqueRowId) !== uniqueRowKeyValue) + .some((row) => !selectedRows.has(rowKeyGetter(row, uniqueRowId)))) + ) { + if (selectedRows.has(rowKeyGetter(parentRow, uniqueRowId))) { + selectedRows.delete(rowKeyGetter(parentRow, uniqueRowId)); + } + } else { + if (parentRow?.childRows && Array.isArray(parentRow.childRows)) { + const isAnyChildUnselected = parentRow.childRows + .filter((row) => rowKeyGetter(row, uniqueRowId) !== uniqueRowKeyValue) + .some((row) => !selectedRows.has(rowKeyGetter(row, uniqueRowId))); + + if (!isAnyChildUnselected) { + parentRow.selected = true; + // instead of pushing the row we should add it to the selected set + selectedRows.add(rowKeyGetter(parentRow, uniqueRowId)); + } + } + } + // add recursiveness if there are more levels + if (parentRow && parentRow.parentKey) { + getParentSelectedState( + rowList, + rowKeyGetter(parentRow, uniqueRowId), + parentRow.parentKey, + uniqueRowId, + selectedRows, + checkedStateToMatch + ); + } +}; diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 7d72d12066..9a720a5017 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -13,6 +13,7 @@ import DxcCheckbox from "./checkbox/Checkbox"; import DxcChip from "./chip/Chip"; import DxcContainer from "./container/Container"; import DxcContextualMenu from "./contextual-menu/ContextualMenu"; +import DxcDataGrid from "./data-grid/DataGrid"; import DxcDateInput from "./date-input/DateInput"; import DxcDialog from "./dialog/Dialog"; import DxcDivider from "./divider/Divider"; @@ -66,6 +67,7 @@ export { DxcChip, DxcContainer, DxcContextualMenu, + DxcDataGrid, DxcDateInput, DxcDialog, DxcDivider, diff --git a/packages/lib/test/accessibility/rules/specific/data-grid/disabledRules.js b/packages/lib/test/accessibility/rules/specific/data-grid/disabledRules.js new file mode 100644 index 0000000000..52c467b57e --- /dev/null +++ b/packages/lib/test/accessibility/rules/specific/data-grid/disabledRules.js @@ -0,0 +1,8 @@ +/** + * Array of accessibility rule IDs to be disabled in both Jest and Storybook for the data grid component. + * + */ +export const disabledRules = [ + // Disable scrollable region focusable rule to prevent errors from having an empty header for the expandable data grids + "empty-table-header", +]; diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 55ba60b1d3..00fd4ab5dc 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -2,7 +2,13 @@ "extends": "@dxc-technology/typescript-config/react-library.json", "compilerOptions": { "outDir": "dist", - "strict": false + "strict": false, + "target": "es5", + "module": "ESNext", // or "es2020" + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true }, "include": ["src"], "exclude": ["node_modules", "dist"]