@astral/ui 4.114.0 → 4.115.1

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.
Files changed (153) hide show
  1. package/components/DataGrid/Body/Body.d.ts +1 -1
  2. package/components/DataGrid/Body/Body.js +7 -3
  3. package/components/DataGrid/Body/types.d.ts +11 -3
  4. package/components/DataGrid/DataGrid.js +0 -1
  5. package/components/DataGrid/PinnedSections/PinnedSections.js +2 -2
  6. package/components/DataGrid/PinnedSections/useLogic/useLogic.d.ts +1 -0
  7. package/components/DataGrid/PinnedSections/useLogic/useLogic.js +4 -3
  8. package/components/DataGrid/Row/Row.js +13 -9
  9. package/components/DataGrid/Row/RowContext/RowContext.d.ts +15 -4
  10. package/components/DataGrid/Row/RowContext/RowContext.js +3 -3
  11. package/components/DataGrid/Row/RowContext/RowProvider/RowProvider.d.ts +10 -1
  12. package/components/DataGrid/Row/RowContext/RowProvider/RowProvider.js +23 -17
  13. package/components/DataGrid/Row/types.d.ts +7 -3
  14. package/components/DataGrid/Row/useLogic/useLogic.d.ts +6 -4
  15. package/components/DataGrid/Row/useLogic/useLogic.js +21 -12
  16. package/components/DataGrid/RowBase/RowBase.js +5 -2
  17. package/components/DataGrid/RowBase/types.d.ts +9 -5
  18. package/components/DataGrid/RowBase/useLogic/useLogic.d.ts +5 -4
  19. package/components/DataGrid/RowBase/useLogic/useLogic.js +16 -10
  20. package/components/DataGrid/RowCheckbox/RowCheckbox.d.ts +5 -0
  21. package/components/DataGrid/RowCheckbox/RowCheckbox.js +13 -0
  22. package/components/DataGrid/RowCheckbox/constants.d.ts +4 -0
  23. package/components/DataGrid/RowCheckbox/constants.js +4 -0
  24. package/components/DataGrid/RowCheckbox/index.d.ts +1 -0
  25. package/components/DataGrid/RowCheckbox/index.js +1 -0
  26. package/components/DataGrid/RowCheckbox/types.d.ts +7 -0
  27. package/components/DataGrid/RowCheckbox/types.js +1 -0
  28. package/components/DataGrid/hooks/index.d.ts +1 -0
  29. package/components/DataGrid/hooks/index.js +1 -0
  30. package/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/index.d.ts +1 -0
  31. package/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/index.js +1 -0
  32. package/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/useWarnRowsWithoutDisabledReason.d.ts +5 -0
  33. package/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/useWarnRowsWithoutDisabledReason.js +17 -0
  34. package/components/DataGrid/index.d.ts +1 -1
  35. package/components/DataGrid/public.d.ts +1 -1
  36. package/components/DataGrid/tests/performance/testDataGridPerformance.d.ts +3 -1
  37. package/components/DataGrid/tests/performance/testDataGridPerformance.js +234 -2
  38. package/components/DataGrid/tests/selection/testDataGridSelection.d.ts +14 -0
  39. package/components/DataGrid/tests/selection/testDataGridSelection.js +562 -0
  40. package/components/DataGrid/types.d.ts +34 -1
  41. package/components/DataGrid/useLogic/useLogic.d.ts +6 -3
  42. package/components/DataGrid/useLogic/useLogic.js +15 -10
  43. package/components/DataGrid/utils/getRowDisabledState/getRowDisabledState.d.ts +31 -0
  44. package/components/DataGrid/utils/getRowDisabledState/getRowDisabledState.js +20 -0
  45. package/components/DataGrid/utils/getRowDisabledState/index.d.ts +1 -0
  46. package/components/DataGrid/utils/getRowDisabledState/index.js +1 -0
  47. package/components/DataGrid/utils/index.d.ts +3 -0
  48. package/components/DataGrid/utils/index.js +3 -0
  49. package/components/DataGrid/utils/resolveRowDisabled/index.d.ts +1 -0
  50. package/components/DataGrid/utils/resolveRowDisabled/index.js +1 -0
  51. package/components/DataGrid/utils/resolveRowDisabled/resolveRowDisabled.d.ts +5 -0
  52. package/components/DataGrid/utils/resolveRowDisabled/resolveRowDisabled.js +17 -0
  53. package/components/DataGrid/utils/resolveRowsDisabled/index.d.ts +1 -0
  54. package/components/DataGrid/utils/resolveRowsDisabled/index.js +1 -0
  55. package/components/DataGrid/utils/resolveRowsDisabled/resolveRowsDisabled.d.ts +16 -0
  56. package/components/DataGrid/utils/resolveRowsDisabled/resolveRowsDisabled.js +17 -0
  57. package/components/DataGridActionCell/hooks/useBlockingActions/useBlockingActions.js +3 -3
  58. package/components/DataGridCollapsibleRow/useLogic/useLogic.d.ts +3 -2
  59. package/components/DataGridCollapsibleRow/useLogic/useLogic.js +4 -3
  60. package/components/DataGridInfinite/DataGridInfinite.js +4 -4
  61. package/components/DataGridInfinite/DataGridInfiniteContext/DataGridInfiniteContext.d.ts +1 -1
  62. package/components/DataGridInfinite/DataGridInfiniteContext/DataGridInfiniteContext.js +1 -1
  63. package/components/DataGridInfinite/Footer/Footer.js +2 -2
  64. package/components/DataGridInfinite/Row/Row.js +2 -2
  65. package/components/DataGridInfinite/Row/index.d.ts +1 -0
  66. package/components/DataGridInfinite/Row/types.d.ts +17 -2
  67. package/components/DataGridInfinite/Row/useLogic/useLogic.d.ts +4 -2
  68. package/components/DataGridInfinite/Row/useLogic/useLogic.js +12 -4
  69. package/components/DataGridInfinite/constants.d.ts +2 -0
  70. package/components/DataGridInfinite/constants.js +2 -0
  71. package/components/DataGridInfinite/faker.d.ts +1 -5
  72. package/components/DataGridInfinite/faker.js +1 -2
  73. package/components/DataGridInfinite/useLogic/useLogic.d.ts +3 -2
  74. package/components/DataGridInfinite/useLogic/useLogic.js +25 -13
  75. package/components/PageContent/styles.js +25 -1
  76. package/components/index.d.ts +1 -1
  77. package/node/components/DataGrid/Body/Body.d.ts +1 -1
  78. package/node/components/DataGrid/Body/Body.js +7 -3
  79. package/node/components/DataGrid/Body/types.d.ts +11 -3
  80. package/node/components/DataGrid/DataGrid.js +0 -1
  81. package/node/components/DataGrid/PinnedSections/PinnedSections.js +2 -2
  82. package/node/components/DataGrid/PinnedSections/useLogic/useLogic.d.ts +1 -0
  83. package/node/components/DataGrid/PinnedSections/useLogic/useLogic.js +4 -3
  84. package/node/components/DataGrid/Row/Row.js +13 -9
  85. package/node/components/DataGrid/Row/RowContext/RowContext.d.ts +15 -4
  86. package/node/components/DataGrid/Row/RowContext/RowContext.js +3 -3
  87. package/node/components/DataGrid/Row/RowContext/RowProvider/RowProvider.d.ts +10 -1
  88. package/node/components/DataGrid/Row/RowContext/RowProvider/RowProvider.js +23 -17
  89. package/node/components/DataGrid/Row/types.d.ts +7 -3
  90. package/node/components/DataGrid/Row/useLogic/useLogic.d.ts +6 -4
  91. package/node/components/DataGrid/Row/useLogic/useLogic.js +23 -14
  92. package/node/components/DataGrid/RowBase/RowBase.js +5 -2
  93. package/node/components/DataGrid/RowBase/types.d.ts +9 -5
  94. package/node/components/DataGrid/RowBase/useLogic/useLogic.d.ts +5 -4
  95. package/node/components/DataGrid/RowBase/useLogic/useLogic.js +16 -10
  96. package/node/components/DataGrid/RowCheckbox/RowCheckbox.d.ts +5 -0
  97. package/node/components/DataGrid/RowCheckbox/RowCheckbox.js +17 -0
  98. package/node/components/DataGrid/RowCheckbox/constants.d.ts +4 -0
  99. package/node/components/DataGrid/RowCheckbox/constants.js +7 -0
  100. package/node/components/DataGrid/RowCheckbox/index.d.ts +1 -0
  101. package/node/components/DataGrid/RowCheckbox/index.js +17 -0
  102. package/node/components/DataGrid/RowCheckbox/types.d.ts +7 -0
  103. package/node/components/DataGrid/RowCheckbox/types.js +2 -0
  104. package/node/components/DataGrid/hooks/index.d.ts +1 -0
  105. package/node/components/DataGrid/hooks/index.js +1 -0
  106. package/node/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/index.d.ts +1 -0
  107. package/node/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/index.js +17 -0
  108. package/node/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/useWarnRowsWithoutDisabledReason.d.ts +5 -0
  109. package/node/components/DataGrid/hooks/useWarnRowsWithoutDisabledReason/useWarnRowsWithoutDisabledReason.js +21 -0
  110. package/node/components/DataGrid/index.d.ts +1 -1
  111. package/node/components/DataGrid/public.d.ts +1 -1
  112. package/node/components/DataGrid/tests/performance/testDataGridPerformance.d.ts +3 -1
  113. package/node/components/DataGrid/tests/performance/testDataGridPerformance.js +232 -0
  114. package/node/components/DataGrid/tests/selection/testDataGridSelection.d.ts +14 -0
  115. package/node/components/DataGrid/tests/selection/testDataGridSelection.js +566 -0
  116. package/node/components/DataGrid/types.d.ts +34 -1
  117. package/node/components/DataGrid/useLogic/useLogic.d.ts +6 -3
  118. package/node/components/DataGrid/useLogic/useLogic.js +17 -12
  119. package/node/components/DataGrid/utils/getRowDisabledState/getRowDisabledState.d.ts +31 -0
  120. package/node/components/DataGrid/utils/getRowDisabledState/getRowDisabledState.js +24 -0
  121. package/node/components/DataGrid/utils/getRowDisabledState/index.d.ts +1 -0
  122. package/node/components/DataGrid/utils/getRowDisabledState/index.js +5 -0
  123. package/node/components/DataGrid/utils/index.d.ts +3 -0
  124. package/node/components/DataGrid/utils/index.js +7 -1
  125. package/node/components/DataGrid/utils/resolveRowDisabled/index.d.ts +1 -0
  126. package/node/components/DataGrid/utils/resolveRowDisabled/index.js +5 -0
  127. package/node/components/DataGrid/utils/resolveRowDisabled/resolveRowDisabled.d.ts +5 -0
  128. package/node/components/DataGrid/utils/resolveRowDisabled/resolveRowDisabled.js +21 -0
  129. package/node/components/DataGrid/utils/resolveRowsDisabled/index.d.ts +1 -0
  130. package/node/components/DataGrid/utils/resolveRowsDisabled/index.js +5 -0
  131. package/node/components/DataGrid/utils/resolveRowsDisabled/resolveRowsDisabled.d.ts +16 -0
  132. package/node/components/DataGrid/utils/resolveRowsDisabled/resolveRowsDisabled.js +21 -0
  133. package/node/components/DataGridActionCell/hooks/useBlockingActions/useBlockingActions.js +3 -3
  134. package/node/components/DataGridCollapsibleRow/useLogic/useLogic.d.ts +3 -2
  135. package/node/components/DataGridCollapsibleRow/useLogic/useLogic.js +4 -3
  136. package/node/components/DataGridInfinite/DataGridInfinite.js +4 -4
  137. package/node/components/DataGridInfinite/DataGridInfiniteContext/DataGridInfiniteContext.d.ts +1 -1
  138. package/node/components/DataGridInfinite/DataGridInfiniteContext/DataGridInfiniteContext.js +1 -1
  139. package/node/components/DataGridInfinite/Footer/Footer.js +1 -1
  140. package/node/components/DataGridInfinite/Row/Row.js +2 -2
  141. package/node/components/DataGridInfinite/Row/index.d.ts +1 -0
  142. package/node/components/DataGridInfinite/Row/types.d.ts +17 -2
  143. package/node/components/DataGridInfinite/Row/useLogic/useLogic.d.ts +4 -2
  144. package/node/components/DataGridInfinite/Row/useLogic/useLogic.js +12 -4
  145. package/node/components/DataGridInfinite/constants.d.ts +2 -0
  146. package/node/components/DataGridInfinite/constants.js +2 -0
  147. package/node/components/DataGridInfinite/faker.d.ts +1 -5
  148. package/node/components/DataGridInfinite/faker.js +1 -2
  149. package/node/components/DataGridInfinite/useLogic/useLogic.d.ts +3 -2
  150. package/node/components/DataGridInfinite/useLogic/useLogic.js +26 -14
  151. package/node/components/PageContent/styles.js +24 -0
  152. package/node/components/index.d.ts +1 -1
  153. package/package.json +1 -1
@@ -0,0 +1,562 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { screen, userEvents, within } from '@ui/dev/tests';
3
+ import { act } from 'react';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { tooltipClassnames } from '../../../Tooltip';
6
+ import { DISABLE_ROW_ATTR } from '../../Row/constants';
7
+ const COLUMNS = [
8
+ { field: 'name', label: 'Наименование' },
9
+ ];
10
+ const COLUMNS_WITH_ACTION = [
11
+ { field: 'name', label: 'Наименование' },
12
+ {
13
+ label: 'Действия',
14
+ renderCell: ({ name }) => _jsxs("button", { type: "button", children: ["\u041E\u0442\u043A\u0440\u044B\u0442\u044C ", name] }),
15
+ },
16
+ ];
17
+ const FIRST_PAGE_ROWS = [
18
+ { id: '1', name: 'Договор №1' },
19
+ { id: '2', name: 'Договор №2' },
20
+ ];
21
+ const SECOND_PAGE_ROWS = [
22
+ { id: '3', name: 'Договор №3' },
23
+ { id: '4', name: 'Договор №4' },
24
+ ];
25
+ const NUMERIC_ID_ROWS = [{ id: 1, name: 'Договор №1' }];
26
+ const SELECTION_DISABLED = {
27
+ mode: 'selection',
28
+ reason: 'Документ отправляется',
29
+ };
30
+ const ROW_DISABLED = {
31
+ mode: 'row',
32
+ reason: 'Нет доступа',
33
+ };
34
+ const ROW_EXCEPT_LAST_CELL_DISABLED = {
35
+ mode: 'row-except-last-cell',
36
+ reason: 'Нет доступа',
37
+ };
38
+ const SELECTION_DISABLED_WITHOUT_REASON = {
39
+ mode: 'selection',
40
+ reason: '',
41
+ };
42
+ const ROW_DISABLED_WITHOUT_REASON = {
43
+ mode: 'row',
44
+ reason: '',
45
+ };
46
+ const INERT_BY_MODE_CASES = [
47
+ { mode: 'row', isInertCell: true, isInertLastCell: true },
48
+ { mode: 'row-except-last-cell', isInertCell: true, isInertLastCell: false },
49
+ { mode: 'selection', isInertCell: false, isInertLastCell: false },
50
+ ];
51
+ const NESTED_ROWS = [
52
+ {
53
+ id: '1',
54
+ name: 'Пакет документов',
55
+ children: [{ id: '1-1', name: 'Вложенный договор' }],
56
+ },
57
+ ];
58
+ const getSelectAllCheckbox = () => within(screen.getByRole('heading')).getByRole('checkbox');
59
+ // closest берёт ближайшую строку, поэтому для вложенной строки не находится родительская
60
+ const getRowCheckbox = (name) => {
61
+ const row = screen.getByText(name).closest('li');
62
+ if (!row) {
63
+ throw new Error(`Строка «${name}» не найдена`);
64
+ }
65
+ return within(row).getByRole('checkbox');
66
+ };
67
+ // Заблокированная ячейка недоступна через атрибут inert
68
+ const checkIsInert = (element) => Boolean(element.closest('[inert]'));
69
+ const getActionButton = (name) => screen.getByRole('button', { name: `Открыть ${name}` });
70
+ const getRowContent = (name) => {
71
+ const rowContent = screen.getByText(name).closest(`[${DISABLE_ROW_ATTR}]`);
72
+ if (!(rowContent instanceof HTMLElement)) {
73
+ throw new Error(`Содержимое строки «${name}» не найдено`);
74
+ }
75
+ return rowContent;
76
+ };
77
+ export const testDataGridSelection = ({ render, renderGrid, }) => {
78
+ const setup = (props = {}) => {
79
+ const onSelectRowSpy = vi.fn();
80
+ const renderUi = (nextProps) => renderGrid({
81
+ rows: FIRST_PAGE_ROWS,
82
+ columns: COLUMNS,
83
+ onSelectRow: onSelectRowSpy,
84
+ onRetry: () => { },
85
+ ...nextProps,
86
+ });
87
+ const view = render(renderUi(props));
88
+ const rerenderGrid = (nextProps) => view.rerender(renderUi(nextProps));
89
+ return { onSelectRowSpy, rerenderGrid };
90
+ };
91
+ it('Выбор строк предыдущей страницы сохраняется при выборе строки на новой странице', async () => {
92
+ const { onSelectRowSpy } = setup({
93
+ rows: SECOND_PAGE_ROWS,
94
+ selectedRows: [FIRST_PAGE_ROWS[0]],
95
+ });
96
+ await userEvents.click(getRowCheckbox('Договор №3'));
97
+ expect(onSelectRowSpy).toHaveBeenCalledWith([
98
+ FIRST_PAGE_ROWS[0],
99
+ SECOND_PAGE_ROWS[0],
100
+ ]);
101
+ });
102
+ it('Строки отмечены выбранными при возврате на страницу с выбранными строками', () => {
103
+ const selectedRows = [FIRST_PAGE_ROWS[0]];
104
+ const { rerenderGrid } = setup({
105
+ rows: SECOND_PAGE_ROWS,
106
+ selectedRows,
107
+ });
108
+ rerenderGrid({ rows: FIRST_PAGE_ROWS, selectedRows });
109
+ expect(getRowCheckbox('Договор №1')).toBeChecked();
110
+ });
111
+ it('Строка выбрана при числовом keyId', () => {
112
+ setup({ rows: NUMERIC_ID_ROWS, selectedRows: NUMERIC_ID_ROWS });
113
+ expect(getRowCheckbox('Договор №1')).toBeChecked();
114
+ });
115
+ it('«Выбрать все» не отмечен при selectedRows только из строк вне выдачи', () => {
116
+ setup({ selectedRows: SECOND_PAGE_ROWS });
117
+ expect(getSelectAllCheckbox()).not.toBeChecked();
118
+ });
119
+ describe('onSelectRow не вызывается', () => {
120
+ it('При новом массиве selectedRows', () => {
121
+ const { rerenderGrid, onSelectRowSpy } = setup({
122
+ selectedRows: [FIRST_PAGE_ROWS[0]],
123
+ });
124
+ rerenderGrid({ selectedRows: [FIRST_PAGE_ROWS[1]] });
125
+ expect(onSelectRowSpy).not.toHaveBeenCalled();
126
+ });
127
+ it('При новом массиве rows с теми же строками', () => {
128
+ const { rerenderGrid, onSelectRowSpy } = setup({
129
+ selectedRows: [FIRST_PAGE_ROWS[0]],
130
+ });
131
+ rerenderGrid({
132
+ rows: [...FIRST_PAGE_ROWS],
133
+ selectedRows: [FIRST_PAGE_ROWS[0]],
134
+ });
135
+ expect(onSelectRowSpy).not.toHaveBeenCalled();
136
+ });
137
+ it('При новой функции getRowDisabled', () => {
138
+ const selectedRows = [FIRST_PAGE_ROWS[0]];
139
+ const { rerenderGrid, onSelectRowSpy } = setup({
140
+ selectedRows,
141
+ getRowDisabled: () => undefined,
142
+ });
143
+ rerenderGrid({
144
+ selectedRows,
145
+ getRowDisabled: () => SELECTION_DISABLED,
146
+ });
147
+ expect(onSelectRowSpy).not.toHaveBeenCalled();
148
+ });
149
+ });
150
+ describe('Выбрать все', () => {
151
+ it('Выбор строк других страниц сохраняется', async () => {
152
+ const { onSelectRowSpy } = setup({
153
+ rows: SECOND_PAGE_ROWS,
154
+ selectedRows: [FIRST_PAGE_ROWS[0]],
155
+ });
156
+ await userEvents.click(getSelectAllCheckbox());
157
+ expect(onSelectRowSpy).toHaveBeenCalledWith([
158
+ FIRST_PAGE_ROWS[0],
159
+ ...SECOND_PAGE_ROWS,
160
+ ]);
161
+ });
162
+ it.each([
163
+ { condition: 'mode=selection', rowDisabled: SELECTION_DISABLED },
164
+ { condition: 'mode=row', rowDisabled: ROW_DISABLED },
165
+ {
166
+ condition: 'mode=row-except-last-cell',
167
+ rowDisabled: ROW_EXCEPT_LAST_CELL_DISABLED,
168
+ },
169
+ {
170
+ condition: 'options.isDisabled=true',
171
+ options: { isDisabled: true },
172
+ },
173
+ {
174
+ condition: 'isNotSelectable=true',
175
+ options: { isNotSelectable: true },
176
+ },
177
+ ])('Строка с $condition не выбирается', async ({ rowDisabled, options }) => {
178
+ const rows = [
179
+ FIRST_PAGE_ROWS[0],
180
+ { ...FIRST_PAGE_ROWS[1], options },
181
+ ];
182
+ const { onSelectRowSpy } = setup({
183
+ rows,
184
+ selectedRows: [],
185
+ getRowDisabled: rowDisabled
186
+ ? (row) => (row.id === '2' ? rowDisabled : undefined)
187
+ : undefined,
188
+ });
189
+ await userEvents.click(getSelectAllCheckbox());
190
+ expect(onSelectRowSpy).toHaveBeenCalledWith([FIRST_PAGE_ROWS[0]]);
191
+ });
192
+ it('Чекбокс отмечен, если выбраны все доступные строки', () => {
193
+ setup({
194
+ selectedRows: [FIRST_PAGE_ROWS[0]],
195
+ getRowDisabled: (row) => row.id === '2' ? SELECTION_DISABLED : undefined,
196
+ });
197
+ expect(getSelectAllCheckbox()).toBeChecked();
198
+ });
199
+ it('Чекбокс в неопределённом состоянии при выборе части доступных строк', () => {
200
+ setup({ selectedRows: [FIRST_PAGE_ROWS[0]] });
201
+ expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'mixed');
202
+ });
203
+ it('Чекбокс не отмечен, если выбрана только строка с mode=selection', () => {
204
+ setup({
205
+ selectedRows: [FIRST_PAGE_ROWS[1]],
206
+ getRowDisabled: (row) => row.id === '2' ? SELECTION_DISABLED : undefined,
207
+ });
208
+ expect(getSelectAllCheckbox()).toHaveAttribute('aria-checked', 'false');
209
+ });
210
+ it('Выбранная строка с mode=selection остаётся выбранной', async () => {
211
+ const { onSelectRowSpy } = setup({
212
+ selectedRows: [FIRST_PAGE_ROWS[1]],
213
+ getRowDisabled: (row) => row.id === '2' ? SELECTION_DISABLED : undefined,
214
+ });
215
+ await userEvents.click(getSelectAllCheckbox());
216
+ expect(onSelectRowSpy).toHaveBeenCalledWith([
217
+ FIRST_PAGE_ROWS[1],
218
+ FIRST_PAGE_ROWS[0],
219
+ ]);
220
+ });
221
+ });
222
+ describe('Снятие «Выбрать все»', () => {
223
+ it('Выбор строк других страниц сохраняется', async () => {
224
+ const { onSelectRowSpy } = setup({
225
+ selectedRows: [SECOND_PAGE_ROWS[0], ...FIRST_PAGE_ROWS],
226
+ });
227
+ await userEvents.click(getSelectAllCheckbox());
228
+ expect(onSelectRowSpy).toHaveBeenCalledWith([SECOND_PAGE_ROWS[0]]);
229
+ });
230
+ it('Выбор строки с mode=selection снимается', async () => {
231
+ const { onSelectRowSpy } = setup({
232
+ selectedRows: FIRST_PAGE_ROWS,
233
+ getRowDisabled: (row) => row.id === '2' ? SELECTION_DISABLED : undefined,
234
+ });
235
+ await userEvents.click(getSelectAllCheckbox());
236
+ expect(onSelectRowSpy).toHaveBeenCalledWith([]);
237
+ });
238
+ });
239
+ describe('Выбранная строка с mode=selection', () => {
240
+ const setupSelectedDisabledRow = () => setup({
241
+ selectedRows: [FIRST_PAGE_ROWS[1]],
242
+ getRowDisabled: (row) => row.id === '2' ? SELECTION_DISABLED : undefined,
243
+ });
244
+ it('Чекбокс строки отмечен', () => {
245
+ setupSelectedDisabledRow();
246
+ expect(getRowCheckbox('Договор №2')).toBeChecked();
247
+ });
248
+ it('Чекбокс строки заблокирован', () => {
249
+ setupSelectedDisabledRow();
250
+ expect(getRowCheckbox('Договор №2')).toBeDisabled();
251
+ });
252
+ });
253
+ describe('Блокировка через getRowDisabled', () => {
254
+ it('Чекбокс строки заблокирован при mode=selection', () => {
255
+ setup({
256
+ getRowDisabled: (row) => row.id === '1' ? SELECTION_DISABLED : undefined,
257
+ });
258
+ expect(getRowCheckbox('Договор №1')).toBeDisabled();
259
+ });
260
+ it('Чекбокс строки заблокирован при mode=row', () => {
261
+ setup({
262
+ getRowDisabled: (row) => (row.id === '1' ? ROW_DISABLED : undefined),
263
+ });
264
+ expect(getRowCheckbox('Договор №1')).toBeDisabled();
265
+ });
266
+ it('onSelectRow не вызывается при клике по checkbox с mode=selection', async () => {
267
+ const { onSelectRowSpy } = setup({
268
+ getRowDisabled: (row) => row.id === '1' ? SELECTION_DISABLED : undefined,
269
+ });
270
+ await userEvents.click(getRowCheckbox('Договор №1'));
271
+ expect(onSelectRowSpy).not.toHaveBeenCalled();
272
+ });
273
+ it('onRowClick вызывается при клике по строке с mode=selection', async () => {
274
+ const onRowClickSpy = vi.fn();
275
+ setup({
276
+ onRowClick: onRowClickSpy,
277
+ getRowDisabled: (row) => row.id === '1' ? SELECTION_DISABLED : undefined,
278
+ });
279
+ await userEvents.click(screen.getByText('Договор №1'));
280
+ expect(onRowClickSpy).toHaveBeenCalledWith(FIRST_PAGE_ROWS[0]);
281
+ });
282
+ it('onRowClick не вызывается при клике по строке с mode=row', async () => {
283
+ const onRowClickSpy = vi.fn();
284
+ setup({
285
+ onRowClick: onRowClickSpy,
286
+ getRowDisabled: (row) => (row.id === '1' ? ROW_DISABLED : undefined),
287
+ });
288
+ await userEvents.click(screen.getByText('Договор №1'));
289
+ expect(onRowClickSpy).not.toHaveBeenCalled();
290
+ });
291
+ it('onRowClick вызывается при mode=selection и options.isDisabled=true', async () => {
292
+ const onRowClickSpy = vi.fn();
293
+ const row = {
294
+ ...FIRST_PAGE_ROWS[0],
295
+ options: { isDisabled: true },
296
+ };
297
+ setup({
298
+ rows: [row],
299
+ onRowClick: onRowClickSpy,
300
+ getRowDisabled: () => SELECTION_DISABLED,
301
+ });
302
+ await userEvents.click(screen.getByText('Договор №1'));
303
+ expect(onRowClickSpy).toHaveBeenCalledWith(row);
304
+ });
305
+ it.each(INERT_BY_MODE_CASES)('Ячейки строки недоступны=$isInertCell при mode=$mode', ({ mode, isInertCell }) => {
306
+ setup({
307
+ columns: COLUMNS_WITH_ACTION,
308
+ getRowDisabled: (row) => row.id === '1' ? { mode, reason: 'Нет доступа' } : undefined,
309
+ });
310
+ expect(checkIsInert(screen.getByText('Договор №1'))).toBe(isInertCell);
311
+ });
312
+ it.each(INERT_BY_MODE_CASES)('Последняя ячейка недоступна=$isInertLastCell при mode=$mode', ({ mode, isInertLastCell }) => {
313
+ setup({
314
+ columns: COLUMNS_WITH_ACTION,
315
+ getRowDisabled: (row) => row.id === '1' ? { mode, reason: 'Нет доступа' } : undefined,
316
+ });
317
+ expect(checkIsInert(getActionButton('Договор №1'))).toBe(isInertLastCell);
318
+ });
319
+ it('Ячейки соседней строки доступны при mode=row', () => {
320
+ setup({
321
+ columns: COLUMNS_WITH_ACTION,
322
+ getRowDisabled: (row) => (row.id === '1' ? ROW_DISABLED : undefined),
323
+ });
324
+ expect(checkIsInert(screen.getByText('Договор №2'))).toBe(false);
325
+ });
326
+ it('Tooltip строки показывает причину при mode=row', async () => {
327
+ setup({
328
+ getRowDisabled: (row) => (row.id === '1' ? ROW_DISABLED : undefined),
329
+ });
330
+ await userEvents.hover(getRowContent('Договор №1'));
331
+ expect(await screen.findByRole('tooltip')).toHaveTextContent(ROW_DISABLED.reason);
332
+ });
333
+ it('Чекбокс строки с options.isDisabled=true заблокирован, если getRowDisabled её не блокирует', () => {
334
+ setup({
335
+ rows: [{ ...FIRST_PAGE_ROWS[0], options: { isDisabled: true } }],
336
+ getRowDisabled: () => undefined,
337
+ });
338
+ expect(getRowCheckbox('Договор №1')).toBeDisabled();
339
+ });
340
+ });
341
+ describe('Блокировка через options без getRowDisabled', () => {
342
+ it('Чекбокс строки заблокирован при options.isDisabled=true', () => {
343
+ setup({
344
+ rows: [{ ...FIRST_PAGE_ROWS[0], options: { isDisabled: true } }],
345
+ });
346
+ expect(getRowCheckbox('Договор №1')).toBeDisabled();
347
+ });
348
+ it('Ячейки строки, кроме последней, недоступны при options.isDisabledLastCell=false', () => {
349
+ setup({
350
+ columns: COLUMNS_WITH_ACTION,
351
+ rows: [
352
+ {
353
+ ...FIRST_PAGE_ROWS[0],
354
+ options: { isDisabled: true, isDisabledLastCell: false },
355
+ },
356
+ ],
357
+ });
358
+ expect(checkIsInert(screen.getByText('Договор №1'))).toBe(true);
359
+ });
360
+ it('Tooltip строки показывает options.disabledReason', async () => {
361
+ setup({
362
+ rows: [
363
+ {
364
+ ...FIRST_PAGE_ROWS[0],
365
+ options: { isDisabled: true, disabledReason: 'Нет доступа' },
366
+ },
367
+ ],
368
+ });
369
+ await userEvents.hover(getRowContent('Договор №1'));
370
+ expect(await screen.findByRole('tooltip')).toHaveTextContent('Нет доступа');
371
+ });
372
+ it('onSelectRow не вызывается при клике по checkbox строки с options.isDisabled=true', async () => {
373
+ const { onSelectRowSpy } = setup({
374
+ rows: [{ ...FIRST_PAGE_ROWS[0], options: { isDisabled: true } }],
375
+ });
376
+ await userEvents.click(getRowCheckbox('Договор №1'));
377
+ expect(onSelectRowSpy).not.toHaveBeenCalled();
378
+ });
379
+ });
380
+ describe('Блокировка без причины', () => {
381
+ const WARN_MESSAGE_PREFIX = 'UIKIT. DataGrid: getRowDisabled вернул блокировку без reason для строк: ';
382
+ let consoleWarnSpy;
383
+ beforeEach(() => {
384
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
385
+ });
386
+ afterEach(() => {
387
+ consoleWarnSpy.mockRestore();
388
+ });
389
+ it('Выводится одно предупреждение со всеми строками без причины', () => {
390
+ setup({ getRowDisabled: () => SELECTION_DISABLED_WITHOUT_REASON });
391
+ expect(consoleWarnSpy.mock.calls).toEqual([
392
+ [`${WARN_MESSAGE_PREFIX}1, 2`],
393
+ ]);
394
+ });
395
+ it('Не выводится повторно при новой функции getRowDisabled с тем же результатом', () => {
396
+ const { rerenderGrid } = setup({
397
+ getRowDisabled: () => ROW_DISABLED_WITHOUT_REASON,
398
+ });
399
+ rerenderGrid({ getRowDisabled: () => ROW_DISABLED_WITHOUT_REASON });
400
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
401
+ });
402
+ it('Выводится только для строк без причины, появившихся позже', () => {
403
+ const getRowDisabled = () => SELECTION_DISABLED_WITHOUT_REASON;
404
+ const { rerenderGrid } = setup({ getRowDisabled });
405
+ rerenderGrid({
406
+ rows: [...FIRST_PAGE_ROWS, ...SECOND_PAGE_ROWS],
407
+ getRowDisabled,
408
+ });
409
+ expect(consoleWarnSpy.mock.calls).toEqual([
410
+ [`${WARN_MESSAGE_PREFIX}1, 2`],
411
+ [`${WARN_MESSAGE_PREFIX}3, 4`],
412
+ ]);
413
+ });
414
+ it('Не выводится для options.isDisabled=true без disabledReason', () => {
415
+ setup({
416
+ rows: [{ ...FIRST_PAGE_ROWS[0], options: { isDisabled: true } }],
417
+ });
418
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
419
+ });
420
+ it('Не выводится при mode=selection с reason', () => {
421
+ setup({ getRowDisabled: () => SELECTION_DISABLED });
422
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
423
+ });
424
+ it('Tooltip не отображается при наведении на строку с mode=row без причины', async () => {
425
+ setup({
426
+ getRowDisabled: (row) => row.id === '1' ? ROW_DISABLED_WITHOUT_REASON : undefined,
427
+ });
428
+ await userEvents.hover(screen.getByText('Договор №1'));
429
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
430
+ });
431
+ });
432
+ describe('Tooltip причины блокировки выбора', () => {
433
+ const user = userEvents.setup({
434
+ advanceTimers: vi.advanceTimersByTime.bind(vi),
435
+ });
436
+ const TOOLTIP_CONTAINER_SELECTOR = `.${tooltipClassnames.contentWrapper}`;
437
+ const getCheckboxTooltipContainer = (name) => {
438
+ const container = getRowCheckbox(name).closest(TOOLTIP_CONTAINER_SELECTOR);
439
+ if (!container) {
440
+ throw new Error(`Контейнер tooltip checkbox строки «${name}» не найден`);
441
+ }
442
+ return container;
443
+ };
444
+ const advanceTime = (ms) => act(() => {
445
+ vi.advanceTimersByTime(ms);
446
+ });
447
+ const setupSelectionDisabledRow = () => setup({
448
+ getRowDisabled: (row) => row.id === '1' ? SELECTION_DISABLED : undefined,
449
+ });
450
+ // shouldAdvanceTime: асинхронная обёртка RTL ждёт setTimeout и без него зависает
451
+ beforeEach(() => {
452
+ vi.useFakeTimers({ shouldAdvanceTime: true });
453
+ });
454
+ afterEach(() => {
455
+ vi.useRealTimers();
456
+ });
457
+ it('Отображается через 300ms после наведения на checkbox с mode=selection', async () => {
458
+ setupSelectionDisabledRow();
459
+ await user.hover(getCheckboxTooltipContainer('Договор №1'));
460
+ advanceTime(300);
461
+ expect(screen.getByRole('tooltip')).toHaveTextContent(SELECTION_DISABLED.reason);
462
+ });
463
+ it('Не отображается раньше 300ms после наведения', async () => {
464
+ setupSelectionDisabledRow();
465
+ await user.hover(getCheckboxTooltipContainer('Договор №1'));
466
+ advanceTime(250);
467
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
468
+ });
469
+ it('Не отображается, если курсор ушёл с checkbox раньше 300ms', async () => {
470
+ setupSelectionDisabledRow();
471
+ await user.hover(getCheckboxTooltipContainer('Договор №1'));
472
+ advanceTime(150);
473
+ await user.unhover(getCheckboxTooltipContainer('Договор №1'));
474
+ advanceTime(300);
475
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
476
+ });
477
+ it('Checkbox строки с mode=row не показывает собственную причину', () => {
478
+ setup({
479
+ getRowDisabled: (row) => (row.id === '1' ? ROW_DISABLED : undefined),
480
+ });
481
+ expect(getRowCheckbox('Договор №1').closest(TOOLTIP_CONTAINER_SELECTOR)).not.toBeInTheDocument();
482
+ });
483
+ });
484
+ describe('Вложенные строки', () => {
485
+ it('Чекбокс вложенной строки заблокирован при mode=selection для неё', () => {
486
+ setup({
487
+ rows: NESTED_ROWS,
488
+ tree: { isInitialExpanded: true },
489
+ getRowDisabled: (row) => row.id === '1-1' ? SELECTION_DISABLED : undefined,
490
+ });
491
+ expect(getRowCheckbox('Вложенный договор')).toBeDisabled();
492
+ });
493
+ it('Чекбокс вложенной строки доступен при mode=row у родителя', () => {
494
+ setup({
495
+ rows: NESTED_ROWS,
496
+ tree: { isInitialExpanded: true },
497
+ getRowDisabled: (row) => (row.id === '1' ? ROW_DISABLED : undefined),
498
+ });
499
+ expect(getRowCheckbox('Вложенный договор')).toBeEnabled();
500
+ });
501
+ it('Чекбокс вложенной строки заблокирован при variant=subrows и mode=selection', () => {
502
+ setup({
503
+ rows: NESTED_ROWS,
504
+ variant: 'subrows',
505
+ getRowDisabled: (row) => row.id === '1-1' ? SELECTION_DISABLED : undefined,
506
+ });
507
+ expect(getRowCheckbox('Вложенный договор')).toBeDisabled();
508
+ });
509
+ describe('mode=row у вложенной строки', () => {
510
+ const setupNestedRowDisabled = () => setup({
511
+ rows: NESTED_ROWS,
512
+ tree: { isInitialExpanded: true },
513
+ getRowDisabled: (row) => row.id === '1-1' ? ROW_DISABLED : undefined,
514
+ });
515
+ it('Ячейки вложенной строки недоступны', () => {
516
+ setupNestedRowDisabled();
517
+ expect(checkIsInert(screen.getByText('Вложенный договор'))).toBe(true);
518
+ });
519
+ it('Ячейки родительской строки доступны', () => {
520
+ setupNestedRowDisabled();
521
+ expect(checkIsInert(screen.getByText('Пакет документов'))).toBe(false);
522
+ });
523
+ });
524
+ describe('options.isDisabled=true у вложенной строки', () => {
525
+ const NESTED_ROWS_WITH_DISABLED_CHILD = [
526
+ {
527
+ ...NESTED_ROWS[0],
528
+ children: [
529
+ {
530
+ id: '1-1',
531
+ name: 'Вложенный договор',
532
+ options: { isDisabled: true },
533
+ },
534
+ ],
535
+ },
536
+ ];
537
+ it('Чекбокс вложенной строки заблокирован без getRowDisabled', () => {
538
+ setup({
539
+ rows: NESTED_ROWS_WITH_DISABLED_CHILD,
540
+ tree: { isInitialExpanded: true },
541
+ });
542
+ expect(getRowCheckbox('Вложенный договор')).toBeDisabled();
543
+ });
544
+ it('Чекбокс вложенной строки заблокирован, если getRowDisabled её не блокирует', () => {
545
+ setup({
546
+ rows: NESTED_ROWS_WITH_DISABLED_CHILD,
547
+ tree: { isInitialExpanded: true },
548
+ getRowDisabled: () => undefined,
549
+ });
550
+ expect(getRowCheckbox('Вложенный договор')).toBeDisabled();
551
+ });
552
+ it('Ячейки вложенной строки доступны при mode=selection из getRowDisabled', () => {
553
+ setup({
554
+ rows: NESTED_ROWS_WITH_DISABLED_CHILD,
555
+ tree: { isInitialExpanded: true },
556
+ getRowDisabled: (row) => row.id === '1-1' ? SELECTION_DISABLED : undefined,
557
+ });
558
+ expect(checkIsInert(screen.getByText('Вложенный договор'))).toBe(false);
559
+ });
560
+ });
561
+ });
562
+ };
@@ -154,6 +154,23 @@ export type DataGridProps<TData extends Record<string, CellValue> = DataGridRow,
154
154
  * Обработчик выбора строки
155
155
  */
156
156
  onSelectRow?: (row: TData[]) => void;
157
+ /**
158
+ * Возвращает блокировку строки { mode, reason }
159
+ * @example
160
+ * const getRowDisabled = useCallback(
161
+ * (row: Document): DataGridRowDisabled | undefined => {
162
+ * if (row.status === 'Отправлен') {
163
+ * return { mode: 'selection', reason: 'Документ уже отправлен получателю' };
164
+ * }
165
+ *
166
+ * return undefined;
167
+ * },
168
+ * [],
169
+ * );
170
+ *
171
+ * <DataGrid getRowDisabled={getRowDisabled} />
172
+ */
173
+ getRowDisabled?: (row: TData) => DataGridRowDisabled | undefined;
157
174
  /**
158
175
  * @example <DataGrid onSort={({fieldId: 'test', sort: 'asc'}) => console.log('sorted')} />
159
176
  * Обработчик сортировки
@@ -318,14 +335,29 @@ export type DataGridRowOptionColumns<TData extends Record<string, CellValue>> =
318
335
  */
319
336
  renderCell?: RenderCell<TData>;
320
337
  };
338
+ /**
339
+ * Состояние блокировки строки
340
+ */
341
+ export type DataGridRowDisabled = {
342
+ /**
343
+ * Тип блокировки
344
+ */
345
+ mode: 'selection' | 'row' | 'row-except-last-cell';
346
+ /**
347
+ * Причина блокировки
348
+ */
349
+ reason: string;
350
+ };
321
351
  export type DataGridRowOptions<TData extends Record<string, CellValue>> = {
322
352
  /**
323
353
  * Если true, строка будет недоступна для взаимодействия
354
+ * @deprecated Используйте `getRowDisabled` с `mode: 'row'`.
324
355
  */
325
356
  isDisabled?: boolean;
326
357
  /**
327
- * Если true, последняя ячейка не будет блокироваться при `isDisabled=true`
358
+ * Если false, последняя ячейка не блокируется при `isDisabled=true`
328
359
  * @default 'true'
360
+ * @deprecated Используйте `getRowDisabled` с `mode: 'row-except-last-cell'`.
329
361
  */
330
362
  isDisabledLastCell?: boolean;
331
363
  /**
@@ -334,6 +366,7 @@ export type DataGridRowOptions<TData extends Record<string, CellValue>> = {
334
366
  isNotSelectable?: boolean;
335
367
  /**
336
368
  * Причина блокировки строки
369
+ * @deprecated Используйте `reason` в `getRowDisabled`.
337
370
  */
338
371
  disabledReason?: string;
339
372
  /**
@@ -1,7 +1,7 @@
1
1
  import { type ChangeEvent } from 'react';
2
- import type { CellValue, DataGridProps, DataGridRow } from '../types';
2
+ import type { CellValue, DataGridProps, DataGridRow, DataGridRowWithOptions } from '../types';
3
3
  type UseLogicParams<TData extends Record<string, CellValue> = DataGridRow, TSortField extends keyof TData = keyof TData> = DataGridProps<TData, TSortField>;
4
- export declare const useLogic: <TData extends Record<string, unknown> = DataGridRow, TSortField extends keyof TData = keyof TData>({ keyId, columns, rows, variant, tree, subrows, selectedRows, isLoading, isDisabled, mobileDisplayVariant, onSelectRow, isError, onRetry, errorMsg, isHideSelectAll, isHideHeaderIfNoData, sorting, className, onSort, maxHeight, gridViewMode: gridViewModeProp, massActions, }: UseLogicParams<TData, TSortField>) => {
4
+ export declare const useLogic: <TData extends Record<string, unknown> = DataGridRow, TSortField extends keyof TData = keyof TData>({ keyId, columns, rows, variant, tree, subrows, selectedRows, isLoading, isDisabled, getRowDisabled, mobileDisplayVariant, onSelectRow, isError, onRetry, errorMsg, isHideSelectAll, isHideHeaderIfNoData, sorting, className, onSort, maxHeight, gridViewMode: gridViewModeProp, massActions, }: UseLogicParams<TData, TSortField>) => {
5
5
  hasMassActions: boolean;
6
6
  isMassActionsOpen: boolean;
7
7
  dataGridClassName: string | undefined;
@@ -12,7 +12,7 @@ export declare const useLogic: <TData extends Record<string, unknown> = DataGrid
12
12
  expandedLevel?: number | undefined;
13
13
  initialVisibleChildrenCount?: number | undefined;
14
14
  } | undefined;
15
- renderRows: TData[];
15
+ renderRows: DataGridRowWithOptions<TData>[];
16
16
  isAllowHorizontalScroll: boolean;
17
17
  isHideHead: boolean;
18
18
  hasPinnedColumns: boolean;
@@ -59,6 +59,9 @@ export declare const useLogic: <TData extends Record<string, unknown> = DataGrid
59
59
  };
60
60
  bodyProps: {
61
61
  isSelectable: boolean;
62
+ selectedIds: Set<TData[keyof TData]>;
63
+ rowsDisabled: Map<unknown, import("../types").DataGridRowDisabled | undefined>;
64
+ getRowDisabled: ((row: TData) => import("../types").DataGridRowDisabled | undefined) | undefined;
62
65
  onSelectRow: (row: TData) => (event: ChangeEvent<HTMLInputElement>) => void;
63
66
  };
64
67
  mobileDisplayVariant: "list" | "cards";