@azrico/reactgrid 1.6.2 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/example.tsx CHANGED
@@ -1,186 +1,202 @@
1
- export * from './views/DataGrid';
2
-
3
- import AzGrid from './views/DataGrid';
4
- import React from 'react';
5
- import './themes/azgrid-theme.css';
6
- import './themes/example-palettes.css';
7
-
8
- const columns = [
9
- {
10
- name: 'name',
11
- title: 'Name',
12
- defaultFlex: 1,
13
- },
14
- {
15
- name: 'job',
16
- title: 'Job',
17
- type: 'combo',
18
- items: ['None', 'Engineer', 'Doctor'],
19
- defaultFlex: 1,
20
- },
21
- {
22
- name: 'age',
23
- title: 'Age',
24
- type: 'number',
25
- defaultFlex: 1,
26
- ariaLabel: (value: any) => `${value} years`,
27
- },
28
- ];
29
-
30
- const editableColumns = ['job', 'age'];
31
-
32
- function getData(delta: any, max = 200) {
33
- const fd: any[] = [];
34
- for (let index = 0; index < max; index++) {
35
- const currentObject = {
36
- __table_id: String(index),
37
- name: 'user-' + index,
38
- age: 5 + index,
39
- job: 'None',
40
- };
41
- if (delta[index] && typeof delta[index] === 'object') {
42
- for (const key in delta[index]) {
43
- currentObject[key] = delta[index][key];
44
- }
45
- }
46
-
47
- const childrows: any[] = [];
48
- if (index % 5 == 0) {
49
- const sub_selfid = 's' + index;
50
- childrows.push({
51
- __table_id: sub_selfid,
52
- __parent__table_id: String(index),
53
- name: 'sub-' + index,
54
- age: 20,
55
- });
56
- }
57
-
58
- currentObject['__children'] = childrows;
59
- fd.push(currentObject);
60
- }
61
- return fd;
62
- }
63
-
64
- export function ExampleGrid() {
65
- const [ds_reset, set_ds_reset] = React.useState(0);
66
- const [delta, set_delta] = React.useState({});
67
- const [delayed_reload, set_delayed_reload] = React.useState(false);
68
-
69
- const dataSource = React.useMemo(() => getData(delta), []);
70
- // const dataSource = React.useCallback(
71
- // async (dt) => {
72
- // //console.log('Start loading');
73
- // return await new Promise((resolve) => {
74
- // setTimeout(() => {
75
- // resolve(getData(delta));
76
- // }, 1000);
77
- // });
78
- // },
79
- // [ds_reset, delta]
80
- // );
81
- // const details_dataSource = React.useCallback(
82
- // async (dt) => {
83
- // //console.log('Start loading');
84
- // return await new Promise((resolve) => {
85
- // setTimeout(() => {
86
- // resolve(getData(delta, 5));
87
- // }, 1000);
88
- // });
89
- // },
90
- // [ds_reset, delta]
91
- // );
92
-
93
- function onEditComplete(params) {
94
- const { value, columnId, rowId, ...rest } = params;
95
- let new_rd = delta[rowId];
96
- if (!new_rd) new_rd = {};
97
- new_rd[columnId] = value;
98
- set_delta((d) => {
99
- return { ...d, [rowId]: new_rd };
100
- });
101
- }
102
-
103
- const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
104
- console.log('dbl', rowdata?.data || rowdata);
105
- alert('dbl: ' + index);
106
- }, []);
107
- const onRowClick = React.useCallback((index: number, rowdata: any) => {
108
- console.log('click:', rowdata?.data || rowdata);
109
- }, []);
110
-
111
- React.useEffect(() => {
112
- setTimeout(() => {
113
- set_delayed_reload(true);
114
- }, 2000);
115
- }, []);
116
- const customColumns = React.useMemo(() => {
117
- if (!delayed_reload) return [];
118
- return [{ name: 'name', render: (r) => r.value + '^' }];
119
- }, [delayed_reload]);
120
-
121
- const contextMenuItems = React.useCallback((product: any) => {
122
- return [
123
- {
124
- type: 'text',
125
- content: product.name,
126
- },
127
- ];
128
- }, []);
129
-
130
- return (
131
- <div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
132
- {['light', 'dark', 'deco'].map((theme) => (
133
- <section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
134
- <h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
135
- <AzGrid
136
- theme={theme}
137
- ariaLabel={`Users (${theme})`}
138
- delayedSize
139
- // details_dataSource={details_dataSource}
140
- dataSource={dataSource}
141
- details_columns={columns}
142
- onRowClick={onRowClick}
143
- onRowDoubleClick={onRowDoubleClick}
144
- allowChildRows
145
- // onSelectionChange={onSelectionChange}
146
- // selected={selected}
147
- contextMenuItems={contextMenuItems as any}
148
- customColumns={customColumns}
149
- columns={columns}
150
- editableColumns={editableColumns}
151
- editable={true}
152
- onEditComplete={onEditComplete}
153
- advancedFilterFields={['name']}
154
- advancedFilterLabel="Search Name"
155
- advancedFilterPlaceholder={<i>Type a name</i>}
156
- // leftColumns={leftColumns}
157
- gridOptions={
158
- {
159
- defaultFilterValue: [{ name: 'name', operator: 'contains', type: 'string', value: '' }],
160
- pagination: 'local',
161
- defaultLimit: 10,
162
- pageSizes: [10, 25, 50],
163
- checkboxColumn: true,
164
- multiSelect: true,
165
- // consumer onRenderRow runs after the internal one
166
- onRenderRow: (rowProps: any) => {
167
- rowProps['data-example-row'] = 'true';
168
- },
169
- // defaultFilterValue: filterValue,
170
- // remoteFilter: false,
171
- // multiSelect: true,
172
- // pagination: 'local',
173
- // defaultLimit: 100,
174
- // limit: 100,
175
- // pageSizes: [100],
176
- // rowIndexColumn: true,
177
- // checkboxColumn: true,
178
- }
179
- }
180
- />
181
- </section>
182
- ))}
183
- </div>
184
- );
185
- }
186
- export default ExampleGrid;
1
+ export * from './views/DataGrid';
2
+
3
+ import AzGrid from './views/DataGrid';
4
+ import React from 'react';
5
+ import './themes/azgrid-theme.css';
6
+ import './themes/example-palettes.css';
7
+
8
+ const columns = [
9
+ {
10
+ name: 'name',
11
+ title: 'Name',
12
+ defaultFlex: 1,
13
+ },
14
+ {
15
+ // typed columns are editable without editableColumns, items keep their case
16
+ name: 'job',
17
+ title: 'Job',
18
+ type: 'combo',
19
+ items: ['None', 'Engineer', 'Doctor'],
20
+ defaultFlex: 1,
21
+ },
22
+ {
23
+ name: 'age',
24
+ title: 'Age',
25
+ type: 'number',
26
+ defaultFlex: 1,
27
+ ariaLabel: (value: any) => `${value} years`,
28
+ },
29
+ {
30
+ // editable only for rows that are not saved yet (no id)
31
+ name: 'location',
32
+ title: 'Location',
33
+ type: 'select',
34
+ items: ['Shelf A', 'Shelf B', 'Back Room'],
35
+ editable: (value: any, cellProps: any) => !cellProps?.data?.id,
36
+ defaultFlex: 1,
37
+ },
38
+ ];
39
+
40
+ // plain columns still need editableColumns
41
+ const editableColumns = ['name'];
42
+
43
+ function getData(delta: any, max = 200) {
44
+ const fd: any[] = [];
45
+ for (let index = 0; index < max; index++) {
46
+ const currentObject = {
47
+ __table_id: String(index),
48
+ name: 'user-' + index,
49
+ age: 5 + index,
50
+ job: 'None',
51
+ location: 'Shelf A',
52
+ // every third row is saved, its location is readonly
53
+ id: index % 3 == 0 ? 'saved-' + index : undefined,
54
+ };
55
+ if (delta[index] && typeof delta[index] === 'object') {
56
+ for (const key in delta[index]) {
57
+ currentObject[key] = delta[index][key];
58
+ }
59
+ }
60
+
61
+ const childrows: any[] = [];
62
+ if (index % 5 == 0) {
63
+ const sub_selfid = 's' + index;
64
+ childrows.push({
65
+ __table_id: sub_selfid,
66
+ __parent__table_id: String(index),
67
+ name: 'sub-' + index,
68
+ age: 20,
69
+ });
70
+ }
71
+
72
+ currentObject['__children'] = childrows;
73
+ fd.push(currentObject);
74
+ }
75
+ return fd;
76
+ }
77
+
78
+ export function ExampleGrid() {
79
+ const [ds_reset, set_ds_reset] = React.useState(0);
80
+ const [delta, set_delta] = React.useState({});
81
+ const [delayed_reload, set_delayed_reload] = React.useState(false);
82
+
83
+ const dataSource = React.useMemo(() => getData(delta), []);
84
+ // const dataSource = React.useCallback(
85
+ // async (dt) => {
86
+ // //console.log('Start loading');
87
+ // return await new Promise((resolve) => {
88
+ // setTimeout(() => {
89
+ // resolve(getData(delta));
90
+ // }, 1000);
91
+ // });
92
+ // },
93
+ // [ds_reset, delta]
94
+ // );
95
+ // const details_dataSource = React.useCallback(
96
+ // async (dt) => {
97
+ // //console.log('Start loading');
98
+ // return await new Promise((resolve) => {
99
+ // setTimeout(() => {
100
+ // resolve(getData(delta, 5));
101
+ // }, 1000);
102
+ // });
103
+ // },
104
+ // [ds_reset, delta]
105
+ // );
106
+
107
+ function onEditComplete(params) {
108
+ const { value, columnId, rowId, ...rest } = params;
109
+ let new_rd = delta[rowId];
110
+ if (!new_rd) new_rd = {};
111
+ new_rd[columnId] = value;
112
+ set_delta((d) => {
113
+ return { ...d, [rowId]: new_rd };
114
+ });
115
+ }
116
+
117
+ const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
118
+ console.log('dbl', rowdata?.data || rowdata);
119
+ alert('dbl: ' + index);
120
+ }, []);
121
+ const onRowClick = React.useCallback((index: number, rowdata: any) => {
122
+ console.log('click:', rowdata?.data || rowdata);
123
+ }, []);
124
+
125
+ React.useEffect(() => {
126
+ setTimeout(() => {
127
+ set_delayed_reload(true);
128
+ }, 2000);
129
+ }, []);
130
+ const customColumns = React.useMemo(() => {
131
+ if (!delayed_reload) return [];
132
+ return [{ name: 'name', render: (r) => r.value + '^' }];
133
+ }, [delayed_reload]);
134
+
135
+ const contextMenuItems = React.useCallback((product: any) => {
136
+ return [
137
+ {
138
+ type: 'text',
139
+ content: product.name,
140
+ },
141
+ { title: 'Log row', onClick: (row) => console.log('context menu: log', row) },
142
+ { title: 'Copy name', onClick: (row) => navigator.clipboard?.writeText(String(row?.name ?? '')) },
143
+ ];
144
+ }, []);
145
+
146
+ return (
147
+ <div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
148
+ {['light', 'dark', 'deco'].map((theme) => (
149
+ <section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
150
+ <h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
151
+ <AzGrid
152
+ theme={theme}
153
+ ariaLabel={`Users (${theme})`}
154
+ delayedSize
155
+ // details_dataSource={details_dataSource}
156
+ dataSource={dataSource}
157
+ details_columns={columns}
158
+ onRowClick={onRowClick}
159
+ onRowDoubleClick={onRowDoubleClick}
160
+ allowChildRows
161
+ // onSelectionChange={onSelectionChange}
162
+ // selected={selected}
163
+ contextMenuItems={contextMenuItems as any}
164
+ customColumns={customColumns}
165
+ columns={columns}
166
+ editableColumns={editableColumns}
167
+ editable={true}
168
+ onEditComplete={onEditComplete}
169
+ advancedFilterFields={['name']}
170
+ advancedFilterLabel="Search Name"
171
+ advancedFilterPlaceholder={<i>Type a name</i>}
172
+ // leftColumns={leftColumns}
173
+ gridOptions={
174
+ {
175
+ defaultFilterValue: [{ name: 'name', operator: 'contains', type: 'string', value: '' }],
176
+ pagination: 'local',
177
+ defaultLimit: 10,
178
+ pageSizes: [10, 25, 50],
179
+ checkboxColumn: true,
180
+ multiSelect: true,
181
+ // consumer onRenderRow runs after the internal one
182
+ onRenderRow: (rowProps: any) => {
183
+ rowProps['data-example-row'] = 'true';
184
+ },
185
+ // defaultFilterValue: filterValue,
186
+ // remoteFilter: false,
187
+ // multiSelect: true,
188
+ // pagination: 'local',
189
+ // defaultLimit: 100,
190
+ // limit: 100,
191
+ // pageSizes: [100],
192
+ // rowIndexColumn: true,
193
+ // checkboxColumn: true,
194
+ }
195
+ }
196
+ />
197
+ </section>
198
+ ))}
199
+ </div>
200
+ );
201
+ }
202
+ export default ExampleGrid;
@@ -5,6 +5,8 @@ import { GridType } from '../types/types';
5
5
  * Inovua's grid renders anonymous divs, so we fill in ARIA grid semantics here.
6
6
  * Cell/header DOM attributes go through the column's cellDOMProps/headerDOMProps
7
7
  * (attributes set on cellProps inside onRender are not forwarded to the DOM).
8
+ * Inovua's prop types only accept an object for headerDOMProps, so the header's
9
+ * aria-colindex/aria-sort are set on the DOM by apply_header_a11y.
8
10
  */
9
11
 
10
12
  export type ColumnA11yOptions = {
@@ -14,6 +16,8 @@ export type ColumnA11yOptions = {
14
16
  gridEditable?: boolean;
15
17
  /** false skips aria-readonly (for interactive special columns like the checkbox column) */
16
18
  markReadonly?: boolean;
19
+ /** column id when the column object has no id/name (inovua's special columns) */
20
+ id?: string;
17
21
  };
18
22
 
19
23
  function set_default(target: any, key: string, value: any) {
@@ -47,6 +51,7 @@ export function apply_column_a11y<T extends Record<string, any>>(
47
51
  options: ColumnA11yOptions = {}
48
52
  ): T {
49
53
  const { role = 'gridcell', gridEditable = false, markReadonly = true } = options;
54
+ const id = col.id ?? col.name ?? options.id;
50
55
  const consumerCellDOMProps = col.cellDOMProps;
51
56
  const consumerHeaderDOMProps = col.headerDOMProps;
52
57
  const ariaLabel = col.ariaLabel;
@@ -66,15 +71,32 @@ export function apply_column_a11y<T extends Record<string, any>>(
66
71
  set_default(res, 'aria-label', ariaLabel(cellProps.value, data));
67
72
  return res;
68
73
  },
69
- headerDOMProps: (cellProps: any) => {
70
- const res = resolve_dom_props(consumerHeaderDOMProps, cellProps);
74
+ headerDOMProps: get_header_dom_props(consumerHeaderDOMProps, id),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * header DOM props as an object (inovua's prop types reject a function),
80
+ * aria-colindex/aria-sort change with the grid state and are set by apply_header_a11y
81
+ * @param consumer consumer's headerDOMProps
82
+ * @param id column id, looked up in the grid's columnsMap
83
+ */
84
+ function get_header_dom_props(consumer: any, id: string | undefined) {
85
+ //a consumer function is kept (inovua runs it), it gets the attributes from the cell props
86
+ if (typeof consumer === 'function')
87
+ return (cellProps: any) => {
88
+ const res = resolve_dom_props(consumer, cellProps);
71
89
  set_default(res, 'role', 'columnheader');
90
+ set_default(res, 'data-column', id);
72
91
  set_default(res, 'aria-colindex', cellProps.computedVisibleIndex + 1);
73
92
  if (cellProps.computedSortable)
74
93
  set_default(res, 'aria-sort', get_sort_dir(cellProps.computedSortInfo));
75
94
  return res;
76
- },
77
- };
95
+ };
96
+ const res = resolve_dom_props(consumer, undefined);
97
+ set_default(res, 'role', 'columnheader');
98
+ set_default(res, 'data-column', id);
99
+ return res;
78
100
  }
79
101
 
80
102
  /**
@@ -115,6 +137,39 @@ export function get_grid_row_count(grid: GridType): number {
115
137
 
116
138
  export type RootA11yOptions = { ariaLabel?: string; editable?: boolean };
117
139
 
140
+ const HEADER_CELL_SELECTOR = '[role="columnheader"][data-column]';
141
+
142
+ /**
143
+ * sets aria-colindex/aria-sort on the header cells of the grid,
144
+ * attributes the consumer set in an object headerDOMProps are left alone
145
+ * @param grid computed props of the grid
146
+ * @param root root element of the grid
147
+ */
148
+ export function apply_header_a11y(grid: GridType | null | undefined, root: Element) {
149
+ const columnsMap = (grid as any)?.columnsMap;
150
+ if (!columnsMap) return;
151
+ root.querySelectorAll(HEADER_CELL_SELECTOR).forEach((el) => {
152
+ //skip headers of grids nested in row details
153
+ if (el.closest('[role="grid"]') !== root) return;
154
+ const col = columnsMap[el.getAttribute('data-column') as string];
155
+ const own = col?.headerDOMProps;
156
+ //a function headerDOMProps sets the attributes itself
157
+ if (!own || typeof own !== 'object') return;
158
+ if (own['aria-colindex'] === undefined)
159
+ set_attr(el, 'aria-colindex', col.computedVisibleIndex + 1);
160
+ if (own['aria-sort'] === undefined)
161
+ set_attr(el, 'aria-sort', col.computedSortable ? get_sort_dir(col.computedSortInfo) : undefined);
162
+ });
163
+ }
164
+
165
+ /**
166
+ * true when a header cell was (re)mounted without its aria-colindex
167
+ * @param root root element of the grid
168
+ */
169
+ export function has_unsynced_header(root: Element | null) {
170
+ return !!root?.querySelector(`${HEADER_CELL_SELECTOR}:not([aria-colindex])`);
171
+ }
172
+
118
173
  /**
119
174
  * true while the grid is loading (loading prop or async datasource)
120
175
  * @param grid computed props of the grid
@@ -145,6 +200,8 @@ export function get_root_a11y_key(grid: GridType | null | undefined, options: Ro
145
200
  options.ariaLabel ?? '',
146
201
  options.editable ? 1 : 0,
147
202
  is_grid_busy(g) ? 1 : 0,
203
+ //header cells: order and sort direction
204
+ (g.visibleColumns || []).map((c) => `${c.id}:${c.computedSortInfo?.dir || 0}`).join(','),
148
205
  ].join('|');
149
206
  }
150
207
 
@@ -165,5 +222,6 @@ export function apply_root_a11y(grid: GridType | null | undefined, options: Root
165
222
  set_attr(el, 'aria-label', options.ariaLabel);
166
223
  set_attr(el, 'aria-readonly', options.editable ? undefined : 'true');
167
224
  set_attr(el, 'aria-busy', is_grid_busy(g) ? 'true' : undefined);
225
+ apply_header_a11y(g, el);
168
226
  return el;
169
227
  }