@axinom/mosaic-ui 0.75.0-rc.5 → 0.77.0
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/dist/THIRD-PARTY-LICENSES.txt +1296 -0
- package/dist/cjs/components/ActionBar/ActionBar.d.ts +26 -0
- package/dist/cjs/components/ActionBar/ActionBar.d.ts.map +1 -0
- package/dist/cjs/components/ActionBar/ActionBar.model.d.ts +49 -0
- package/dist/cjs/components/ActionBar/ActionBar.model.d.ts.map +1 -0
- package/dist/cjs/components/ActionBar/ActionBarButton/ActionBarButton.d.ts +22 -0
- package/dist/cjs/components/ActionBar/ActionBarButton/ActionBarButton.d.ts.map +1 -0
- package/dist/cjs/components/ActionBar/helpers.d.ts +22 -0
- package/dist/cjs/components/ActionBar/helpers.d.ts.map +1 -0
- package/dist/cjs/components/ActionBar/index.d.ts +3 -0
- package/dist/cjs/components/ActionBar/index.d.ts.map +1 -0
- package/dist/cjs/components/Actions/Action/Action.d.ts.map +1 -1
- package/dist/cjs/components/ConfirmDialog/index.d.ts +1 -0
- package/dist/cjs/components/ConfirmDialog/index.d.ts.map +1 -1
- package/dist/cjs/components/ConfirmDialog/useConfirmableAction.d.ts +55 -0
- package/dist/cjs/components/ConfirmDialog/useConfirmableAction.d.ts.map +1 -0
- package/dist/cjs/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
- package/dist/cjs/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/cjs/components/Explorer/QuickEdit/useQuickEdit.d.ts +14 -3
- package/dist/cjs/components/Explorer/QuickEdit/useQuickEdit.d.ts.map +1 -1
- package/dist/cjs/components/Explorer/helpers/useActions.d.ts +15 -4
- package/dist/cjs/components/Explorer/helpers/useActions.d.ts.map +1 -1
- package/dist/cjs/components/Explorer/helpers/useSubtitle.d.ts.map +1 -1
- package/dist/cjs/components/List/List.d.ts +17 -3
- package/dist/cjs/components/List/List.d.ts.map +1 -1
- package/dist/cjs/components/List/List.model.d.ts +5 -1
- package/dist/cjs/components/List/List.model.d.ts.map +1 -1
- package/dist/cjs/components/List/ListHeader/ListHeader.d.ts +10 -1
- package/dist/cjs/components/List/ListHeader/ListHeader.d.ts.map +1 -1
- package/dist/cjs/components/List/ListRow/ListRow.d.ts +16 -5
- package/dist/cjs/components/List/ListRow/ListRow.d.ts.map +1 -1
- package/dist/cjs/components/List/ListRowRenderer/ListRowRenderer.d.ts.map +1 -1
- package/dist/cjs/components/List/helpers.d.ts +11 -3
- package/dist/cjs/components/List/helpers.d.ts.map +1 -1
- package/dist/cjs/components/List/useColumnsSize.d.ts +4 -2
- package/dist/cjs/components/List/useColumnsSize.d.ts.map +1 -1
- package/dist/cjs/components/PageHeader/PageHeader.d.ts +4 -1
- package/dist/cjs/components/PageHeader/PageHeader.d.ts.map +1 -1
- package/dist/cjs/components/PageHeader/PageHeader.model.d.ts +37 -1
- package/dist/cjs/components/PageHeader/PageHeader.model.d.ts.map +1 -1
- package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts +3 -0
- package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
- package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +8 -0
- package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
- package/dist/cjs/components/PageHeader/helpers.d.ts +9 -0
- package/dist/cjs/components/PageHeader/helpers.d.ts.map +1 -0
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/index.d.ts.map +1 -1
- package/dist/cjs/hooks/index.d.ts +1 -0
- package/dist/cjs/hooks/index.d.ts.map +1 -1
- package/dist/cjs/hooks/useElementWidthObserver/useElementWidthObserver.d.ts.map +1 -0
- package/dist/cjs/index.js +4 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/components/ActionBar/ActionBar.d.ts +26 -0
- package/dist/esm/components/ActionBar/ActionBar.d.ts.map +1 -0
- package/dist/esm/components/ActionBar/ActionBar.model.d.ts +49 -0
- package/dist/esm/components/ActionBar/ActionBar.model.d.ts.map +1 -0
- package/dist/esm/components/ActionBar/ActionBarButton/ActionBarButton.d.ts +22 -0
- package/dist/esm/components/ActionBar/ActionBarButton/ActionBarButton.d.ts.map +1 -0
- package/dist/esm/components/ActionBar/helpers.d.ts +22 -0
- package/dist/esm/components/ActionBar/helpers.d.ts.map +1 -0
- package/dist/esm/components/ActionBar/index.d.ts +3 -0
- package/dist/esm/components/ActionBar/index.d.ts.map +1 -0
- package/dist/esm/components/Actions/Action/Action.d.ts.map +1 -1
- package/dist/esm/components/ConfirmDialog/index.d.ts +1 -0
- package/dist/esm/components/ConfirmDialog/index.d.ts.map +1 -1
- package/dist/esm/components/ConfirmDialog/useConfirmableAction.d.ts +55 -0
- package/dist/esm/components/ConfirmDialog/useConfirmableAction.d.ts.map +1 -0
- package/dist/esm/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
- package/dist/esm/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/esm/components/Explorer/QuickEdit/useQuickEdit.d.ts +14 -3
- package/dist/esm/components/Explorer/QuickEdit/useQuickEdit.d.ts.map +1 -1
- package/dist/esm/components/Explorer/helpers/useActions.d.ts +15 -4
- package/dist/esm/components/Explorer/helpers/useActions.d.ts.map +1 -1
- package/dist/esm/components/Explorer/helpers/useSubtitle.d.ts.map +1 -1
- package/dist/esm/components/List/List.d.ts +17 -3
- package/dist/esm/components/List/List.d.ts.map +1 -1
- package/dist/esm/components/List/List.model.d.ts +5 -1
- package/dist/esm/components/List/List.model.d.ts.map +1 -1
- package/dist/esm/components/List/ListHeader/ListHeader.d.ts +10 -1
- package/dist/esm/components/List/ListHeader/ListHeader.d.ts.map +1 -1
- package/dist/esm/components/List/ListRow/ListRow.d.ts +16 -5
- package/dist/esm/components/List/ListRow/ListRow.d.ts.map +1 -1
- package/dist/esm/components/List/ListRowRenderer/ListRowRenderer.d.ts.map +1 -1
- package/dist/esm/components/List/helpers.d.ts +11 -3
- package/dist/esm/components/List/helpers.d.ts.map +1 -1
- package/dist/esm/components/List/useColumnsSize.d.ts +4 -2
- package/dist/esm/components/List/useColumnsSize.d.ts.map +1 -1
- package/dist/esm/components/PageHeader/PageHeader.d.ts +4 -1
- package/dist/esm/components/PageHeader/PageHeader.d.ts.map +1 -1
- package/dist/esm/components/PageHeader/PageHeader.model.d.ts +37 -1
- package/dist/esm/components/PageHeader/PageHeader.model.d.ts.map +1 -1
- package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts +3 -0
- package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
- package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +8 -0
- package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
- package/dist/esm/components/PageHeader/helpers.d.ts +9 -0
- package/dist/esm/components/PageHeader/helpers.d.ts.map +1 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.d.ts.map +1 -1
- package/dist/esm/hooks/index.d.ts +1 -0
- package/dist/esm/hooks/index.d.ts.map +1 -1
- package/dist/esm/hooks/useElementWidthObserver/useElementWidthObserver.d.ts.map +1 -0
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/package.json +7 -3
- package/src/components/ActionBar/ActionBar.model.ts +61 -0
- package/src/components/ActionBar/ActionBar.module.scss +64 -0
- package/src/components/ActionBar/ActionBar.spec.tsx +398 -0
- package/src/components/ActionBar/ActionBar.stories.tsx +205 -0
- package/src/components/ActionBar/ActionBar.tsx +170 -0
- package/src/components/ActionBar/ActionBarButton/ActionBarButton.module.scss +140 -0
- package/src/components/ActionBar/ActionBarButton/ActionBarButton.tsx +193 -0
- package/src/components/ActionBar/helpers.spec.ts +41 -0
- package/src/components/ActionBar/helpers.ts +54 -0
- package/src/components/ActionBar/index.ts +2 -0
- package/src/components/Actions/Action/Action.spec.tsx +64 -2
- package/src/components/Actions/Action/Action.tsx +6 -1
- package/src/components/ConfirmDialog/index.ts +1 -0
- package/src/components/ConfirmDialog/useConfirmableAction.tsx +159 -0
- package/src/components/EmptyStation/EmptyStation.stories.tsx +97 -0
- package/src/components/Explorer/BulkEdit/useBulkEdit.tsx +2 -0
- package/src/components/Explorer/Explorer.spec.tsx +97 -8
- package/src/components/Explorer/Explorer.tsx +26 -13
- package/src/components/Explorer/NavigationExplorer/NavigationExplorer.stories.tsx +124 -1
- package/src/components/Explorer/QuickEdit/useQuickEdit.spec.tsx +94 -50
- package/src/components/Explorer/QuickEdit/useQuickEdit.tsx +101 -42
- package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +46 -13
- package/src/components/Explorer/helpers/useActions.ts +125 -73
- package/src/components/Explorer/helpers/useSubtitle.spec.tsx +33 -23
- package/src/components/Explorer/helpers/useSubtitle.tsx +18 -26
- package/src/components/List/List.model.ts +5 -1
- package/src/components/List/List.module.scss +25 -6
- package/src/components/List/List.spec.tsx +498 -1
- package/src/components/List/List.stories.tsx +137 -8
- package/src/components/List/List.tsx +305 -194
- package/src/components/List/ListHeader/ListHeader.module.scss +14 -0
- package/src/components/List/ListHeader/ListHeader.spec.tsx +1 -0
- package/src/components/List/ListHeader/ListHeader.tsx +25 -8
- package/src/components/List/ListRow/ListRow.module.scss +1 -1
- package/src/components/List/ListRow/ListRow.spec.tsx +109 -22
- package/src/components/List/ListRow/ListRow.tsx +52 -13
- package/src/components/List/ListRowRenderer/ListRowRenderer.tsx +28 -22
- package/src/components/List/helpers.spec.ts +29 -0
- package/src/components/List/helpers.ts +17 -9
- package/src/components/List/useColumnsSize.ts +17 -7
- package/src/components/PageHeader/PageHeader.model.ts +38 -1
- package/src/components/PageHeader/PageHeader.module.scss +39 -18
- package/src/components/PageHeader/PageHeader.spec.tsx +407 -18
- package/src/components/PageHeader/PageHeader.stories.tsx +95 -12
- package/src/components/PageHeader/PageHeader.tsx +89 -38
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.model.ts +8 -0
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.module.scss +50 -42
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.spec.tsx +193 -1
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.stories.tsx +36 -0
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +81 -130
- package/src/components/PageHeader/helpers.spec.ts +60 -0
- package/src/components/PageHeader/helpers.ts +36 -0
- package/src/components/index.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/styles/variables.scss +41 -7
- package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +0 -35
- package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +0 -1
- package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts +0 -6
- package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts.map +0 -1
- package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts +0 -3
- package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts.map +0 -1
- package/dist/cjs/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +0 -1
- package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +0 -35
- package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +0 -1
- package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts +0 -6
- package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts.map +0 -1
- package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts +0 -3
- package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts.map +0 -1
- package/dist/esm/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +0 -1
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.module.scss +0 -77
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.spec.tsx +0 -122
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +0 -235
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.ts +0 -15
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.tsx +0 -37
- /package/dist/cjs/{components/PageHeader/helpers → hooks/useElementWidthObserver}/useElementWidthObserver.d.ts +0 -0
- /package/dist/esm/{components/PageHeader/helpers → hooks/useElementWidthObserver}/useElementWidthObserver.d.ts +0 -0
- /package/src/{components/PageHeader/helpers → hooks/useElementWidthObserver}/useElementWidthObserver.tsx +0 -0
|
@@ -6,12 +6,14 @@ import React, {
|
|
|
6
6
|
useCallback,
|
|
7
7
|
useEffect,
|
|
8
8
|
useImperativeHandle,
|
|
9
|
+
useMemo,
|
|
9
10
|
useRef,
|
|
10
11
|
useState,
|
|
11
12
|
} from 'react';
|
|
12
13
|
import type { ActionData } from '..';
|
|
13
14
|
import { noop } from '../../helpers/utils';
|
|
14
15
|
import type { Data } from '../../types/data';
|
|
16
|
+
import { ActionBar, type ActionBarData } from '../ActionBar';
|
|
15
17
|
import { ButtonContext } from '../Buttons/Button.model';
|
|
16
18
|
import { TextButton } from '../Buttons/TextButton/TextButton';
|
|
17
19
|
import {
|
|
@@ -27,7 +29,7 @@ import { ListHeader } from './ListHeader/ListHeader';
|
|
|
27
29
|
import type { ListRowProps } from './ListRow/ListRow';
|
|
28
30
|
import { ListRowLoader } from './ListRow/ListRowLoader';
|
|
29
31
|
import { ListRowRenderer } from './ListRowRenderer/ListRowRenderer';
|
|
30
|
-
import { getActionButtonVisibility,
|
|
32
|
+
import { getActionButtonVisibility, getTriggerIndex, useSort } from './helpers';
|
|
31
33
|
import { useColumnsSize } from './useColumnsSize';
|
|
32
34
|
|
|
33
35
|
export interface ListProps<T extends Data> {
|
|
@@ -44,7 +46,7 @@ export interface ListProps<T extends Data> {
|
|
|
44
46
|
isLoading?: boolean;
|
|
45
47
|
/** Defines whether there was an error with the data. If true a button will be shown that will trigger `onRetry` when hit. */
|
|
46
48
|
isError?: boolean;
|
|
47
|
-
/** Optional error message to display if an error occurs. (default: 'There was an error.) */
|
|
49
|
+
/** Optional error message to display if an error occurs. (default: 'There was an error.') */
|
|
48
50
|
errorMsg?: string;
|
|
49
51
|
/** Determines whether the component should render the error message and the 'onRetry' button. Set this to false if error handling will be done outside this component. (default: true) */
|
|
50
52
|
handleRetry?: boolean;
|
|
@@ -90,13 +92,16 @@ export interface ListProps<T extends Data> {
|
|
|
90
92
|
/**
|
|
91
93
|
* When set, this function is used to generate the link that the user should be navigated to for each item.
|
|
92
94
|
*
|
|
93
|
-
* This property takes precedence over `onItemClicked`, which
|
|
95
|
+
* This property takes precedence over `onItemClicked`, which is only executed
|
|
96
|
+
* while the link is suppressed, i.e. when the list is in a selection mode or
|
|
97
|
+
* the row is disabled.
|
|
94
98
|
*/
|
|
95
99
|
generateItemLink?: (data: T) => LocationDescriptor<unknown>;
|
|
96
100
|
/**
|
|
97
101
|
* Raised when an data item is clicked.
|
|
98
102
|
*
|
|
99
|
-
*
|
|
103
|
+
* If `generateItemLink` is set, this is only raised while the list is in a
|
|
104
|
+
* selection mode or the row is disabled.
|
|
100
105
|
*/
|
|
101
106
|
onItemClicked?: (data: T) => void | Promise<void>;
|
|
102
107
|
/** Raised when item selection has changed */
|
|
@@ -109,6 +114,16 @@ export interface ListProps<T extends Data> {
|
|
|
109
114
|
onRetry?: () => void;
|
|
110
115
|
/** CSS Class name for additional styles */
|
|
111
116
|
className?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Content of the bar that sits on top of the column header, holding the
|
|
119
|
+
* actions of whichever context is currently active.
|
|
120
|
+
*
|
|
121
|
+
* Consumers that offer several sets of actions, e.g. bulk actions and quick
|
|
122
|
+
* edit, resolve which of them is active and pass that one - only a single set
|
|
123
|
+
* can be on display at a time. The bar collapses while it has neither a label
|
|
124
|
+
* nor any actions to show.
|
|
125
|
+
*/
|
|
126
|
+
actionBar?: ActionBarData;
|
|
112
127
|
/** Provide inline actions which are available through '...' context menu */
|
|
113
128
|
inlineMenuActions?: (data: T) => ActionData[];
|
|
114
129
|
/** Function to determine if a specific row should be disabled */
|
|
@@ -117,6 +132,31 @@ export interface ListProps<T extends Data> {
|
|
|
117
132
|
customRowRenderer?: (props: ListRowProps<T>) => ReactElement | null;
|
|
118
133
|
}
|
|
119
134
|
|
|
135
|
+
interface Selection {
|
|
136
|
+
/** Keys of the rows that are selected individually. */
|
|
137
|
+
keys: ReadonlySet<string>;
|
|
138
|
+
/**
|
|
139
|
+
* Set while every row counts as selected without being tracked individually:
|
|
140
|
+
* the header 'select all' checkbox is checked, or the user has checked every
|
|
141
|
+
* row while `enableSelectAllDeselect` is on. Rows that are loaded later are
|
|
142
|
+
* selected too, so this cannot be derived from `keys` and `data`.
|
|
143
|
+
*/
|
|
144
|
+
isAll: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Shared empty selection, so that clearing twice does not re-render. */
|
|
148
|
+
const EMPTY_SELECTION: Selection = { keys: new Set<string>(), isAll: false };
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Property used to identify a row when no `keyProperty` is given. `Data` allows
|
|
152
|
+
* any string index, so this holds for every `T` even though TypeScript cannot
|
|
153
|
+
* prove that 'id' is one of `keyof T`.
|
|
154
|
+
*/
|
|
155
|
+
const DEFAULT_KEY_PROPERTY = 'id';
|
|
156
|
+
|
|
157
|
+
/** Prefix for the positional fallback key, to keep it out of real key values. */
|
|
158
|
+
const INDEX_KEY_PREFIX = '__list_index_';
|
|
159
|
+
|
|
120
160
|
const ListRenderer = <T extends Data>(
|
|
121
161
|
{
|
|
122
162
|
columns,
|
|
@@ -134,7 +174,7 @@ const ListRenderer = <T extends Data>(
|
|
|
134
174
|
headerRowActionSize = '28px',
|
|
135
175
|
horizontalTextAlign = 'left',
|
|
136
176
|
verticalTextAlign = 'center',
|
|
137
|
-
keyProperty
|
|
177
|
+
keyProperty,
|
|
138
178
|
showActionButton = true,
|
|
139
179
|
loadingTriggerOffset = 10,
|
|
140
180
|
defaultSortOrder,
|
|
@@ -149,158 +189,212 @@ const ListRenderer = <T extends Data>(
|
|
|
149
189
|
onRetry = noop,
|
|
150
190
|
generateItemLink,
|
|
151
191
|
className = '',
|
|
192
|
+
actionBar,
|
|
152
193
|
inlineMenuActions,
|
|
153
194
|
isRowDisabled,
|
|
154
195
|
customRowRenderer,
|
|
155
196
|
}: PropsWithChildren<ListProps<T>>,
|
|
156
197
|
ref?: React.ForwardedRef<ListElement>,
|
|
157
198
|
): JSX.Element => {
|
|
158
|
-
const [
|
|
159
|
-
|
|
199
|
+
const [selection, setSelection] = useState<Selection>(EMPTY_SELECTION);
|
|
200
|
+
/**
|
|
201
|
+
* Mirrors `selection` so that handlers can read-modify-write it without
|
|
202
|
+
* waiting for a re-render, which keeps several toggles within one batch from
|
|
203
|
+
* overwriting each other. Never read while rendering - `selection` is what
|
|
204
|
+
* drives the output.
|
|
205
|
+
*/
|
|
206
|
+
const selectionRef = useRef<Selection>(selection);
|
|
207
|
+
|
|
208
|
+
const applySelection = useCallback((next: Selection): void => {
|
|
209
|
+
selectionRef.current = next;
|
|
210
|
+
setSelection(next);
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const rowKeyProperty = (keyProperty ?? DEFAULT_KEY_PROPERTY) as keyof T;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Resolves the stable identity of a row. Falls back to the row position if
|
|
217
|
+
* the data carries no value for `keyProperty`, in which case the selection
|
|
218
|
+
* cannot survive a re-ordering of `data` (see the warning below).
|
|
219
|
+
*/
|
|
220
|
+
const getRowKey = useCallback(
|
|
221
|
+
(row: T, index: number): string => {
|
|
222
|
+
const key = row[rowKeyProperty];
|
|
223
|
+
return key == null ? `${INDEX_KEY_PREFIX}${index}` : String(key);
|
|
224
|
+
},
|
|
225
|
+
[rowKeyProperty],
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const listItems = useMemo<ListItem<T>[]>(
|
|
229
|
+
() =>
|
|
230
|
+
data.map((row, index) => ({
|
|
231
|
+
selected: selection.isAll || selection.keys.has(getRowKey(row, index)),
|
|
232
|
+
data: row,
|
|
233
|
+
})),
|
|
234
|
+
[data, getRowKey, selection],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const hasWarnedAboutKeyProperty = useRef<boolean>(false);
|
|
160
238
|
|
|
161
239
|
useEffect(() => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
240
|
+
if (
|
|
241
|
+
hasWarnedAboutKeyProperty.current ||
|
|
242
|
+
selectionMode === ListSelectMode.None ||
|
|
243
|
+
data.length === 0 ||
|
|
244
|
+
data[0][rowKeyProperty] != null
|
|
245
|
+
) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
hasWarnedAboutKeyProperty.current = true;
|
|
250
|
+
// eslint-disable-next-line no-console
|
|
251
|
+
console.warn(
|
|
252
|
+
`List: 'selectionMode' is enabled but the data has no '${String(
|
|
253
|
+
rowKeyProperty,
|
|
254
|
+
)}' property. The selection is tracked by row position and will apply to the wrong rows once the data is sorted or an item is removed. Set 'keyProperty' to a property that identifies a row.`,
|
|
177
255
|
);
|
|
256
|
+
}, [data, rowKeyProperty, selectionMode]);
|
|
178
257
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
258
|
+
const {
|
|
259
|
+
columnSizes,
|
|
260
|
+
resetColumnSizes,
|
|
261
|
+
setColumnSizes,
|
|
262
|
+
hasActionColumn,
|
|
263
|
+
actionSlotSize,
|
|
264
|
+
} = useColumnsSize(
|
|
265
|
+
columns,
|
|
266
|
+
Boolean(showActionButton),
|
|
267
|
+
selectionMode,
|
|
268
|
+
Boolean(inlineMenuActions),
|
|
269
|
+
);
|
|
184
270
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
isAllItemsChecked.current = true;
|
|
191
|
-
} else {
|
|
192
|
-
isAllItemsChecked.current = false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
onItemSelected &&
|
|
196
|
-
onItemSelected({
|
|
197
|
-
mode: 'SINGLE_ITEMS',
|
|
198
|
-
items: selectedData,
|
|
199
|
-
});
|
|
200
|
-
},
|
|
201
|
-
[enableSelectAllDeselect, listItems.length, onItemSelected],
|
|
271
|
+
// `minimumWidth` sizes the columns, so it belongs to the container that holds
|
|
272
|
+
// them rather than to the frame around it.
|
|
273
|
+
const listStyles = useMemo(
|
|
274
|
+
() => ({ gridRowGap: rowGap, minWidth: minimumWidth }),
|
|
275
|
+
[minimumWidth, rowGap],
|
|
202
276
|
);
|
|
203
277
|
|
|
278
|
+
const clearSelection = useCallback((): void => {
|
|
279
|
+
applySelection(EMPTY_SELECTION);
|
|
280
|
+
|
|
281
|
+
onItemSelected({
|
|
282
|
+
mode: 'SINGLE_ITEMS',
|
|
283
|
+
items: [],
|
|
284
|
+
});
|
|
285
|
+
}, [applySelection, onItemSelected]);
|
|
286
|
+
|
|
204
287
|
const headerCheckboxHandler = useCallback(
|
|
205
288
|
(checked: boolean): void => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
})),
|
|
289
|
+
applySelection({ keys: EMPTY_SELECTION.keys, isAll: checked });
|
|
290
|
+
|
|
291
|
+
onItemSelected(
|
|
292
|
+
checked ? { mode: 'SELECT_ALL' } : { mode: 'SINGLE_ITEMS', items: [] },
|
|
211
293
|
);
|
|
294
|
+
},
|
|
295
|
+
[applySelection, onItemSelected],
|
|
296
|
+
);
|
|
212
297
|
|
|
213
|
-
|
|
298
|
+
const itemSelectedHandler = useCallback(
|
|
299
|
+
(selected: boolean, key: string): void => {
|
|
300
|
+
const current = selectionRef.current;
|
|
301
|
+
// Toggling a row while 'select all' is active turns the implicit
|
|
302
|
+
// selection into an explicit one, so that the other rows stay selected.
|
|
303
|
+
const nextKeys = new Set(
|
|
304
|
+
current.isAll ? data.map(getRowKey) : current.keys,
|
|
305
|
+
);
|
|
214
306
|
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
mode: 'SELECT_ALL',
|
|
218
|
-
});
|
|
307
|
+
if (selected) {
|
|
308
|
+
nextKeys.add(key);
|
|
219
309
|
} else {
|
|
220
|
-
|
|
221
|
-
mode: 'SINGLE_ITEMS',
|
|
222
|
-
items: [],
|
|
223
|
-
});
|
|
310
|
+
nextKeys.delete(key);
|
|
224
311
|
}
|
|
225
|
-
},
|
|
226
|
-
[onItemSelected],
|
|
227
|
-
);
|
|
228
312
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
313
|
+
applySelection({
|
|
314
|
+
keys: nextKeys,
|
|
315
|
+
isAll:
|
|
316
|
+
enableSelectAllDeselect &&
|
|
317
|
+
data.length > 0 &&
|
|
318
|
+
nextKeys.size === data.length,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
onItemSelected({
|
|
322
|
+
mode: 'SINGLE_ITEMS',
|
|
323
|
+
items: data.filter((row, index) => nextKeys.has(getRowKey(row, index))),
|
|
324
|
+
});
|
|
235
325
|
},
|
|
236
|
-
[
|
|
326
|
+
[applySelection, data, enableSelectAllDeselect, getRowKey, onItemSelected],
|
|
237
327
|
);
|
|
238
328
|
|
|
239
|
-
const onTriggeredHandler = (): void => {
|
|
329
|
+
const onTriggeredHandler = useCallback((): void => {
|
|
240
330
|
if (!isLoading && !isError) {
|
|
241
331
|
onRequestMoreData();
|
|
242
332
|
}
|
|
243
|
-
};
|
|
333
|
+
}, [isError, isLoading, onRequestMoreData]);
|
|
244
334
|
|
|
245
|
-
const itemClickedHandler = async (
|
|
335
|
+
const itemClickedHandler = async (row: T, index: number): Promise<void> => {
|
|
246
336
|
try {
|
|
247
|
-
await onItemClicked(
|
|
337
|
+
await onItemClicked(row);
|
|
248
338
|
|
|
249
339
|
if (selectionMode === ListSelectMode.Single) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})),
|
|
255
|
-
);
|
|
340
|
+
applySelection({
|
|
341
|
+
keys: new Set([getRowKey(row, index)]),
|
|
342
|
+
isAll: false,
|
|
343
|
+
});
|
|
256
344
|
}
|
|
257
|
-
} catch
|
|
345
|
+
} catch {
|
|
346
|
+
// The consumer rejected the click, so the selection is left untouched.
|
|
258
347
|
return;
|
|
259
348
|
}
|
|
260
349
|
};
|
|
261
350
|
|
|
262
351
|
const { sort, sortChangedHandler } = useSort(defaultSortOrder, onSortChanged);
|
|
263
352
|
|
|
353
|
+
// Intentionally without a dependency array: the handle reads `listItems` to
|
|
354
|
+
// resolve the index, so it has to be rebuilt whenever the rows change.
|
|
264
355
|
useImperativeHandle(ref, () => ({
|
|
265
|
-
resetSelection:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
selected: false,
|
|
269
|
-
data: i.data,
|
|
270
|
-
})),
|
|
271
|
-
);
|
|
356
|
+
resetSelection: clearSelection,
|
|
357
|
+
selectIndex: (index: number) => {
|
|
358
|
+
const item = listItems[index];
|
|
272
359
|
|
|
273
|
-
|
|
360
|
+
// Nothing at that position, or the list renders no selection at all - in
|
|
361
|
+
// which case selecting would raise `onItemSelected` for something the
|
|
362
|
+
// user can neither see nor undo.
|
|
363
|
+
if (!item || selectionMode === ListSelectMode.None) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
274
366
|
|
|
275
|
-
onItemSelected({
|
|
276
|
-
mode: 'SINGLE_ITEMS',
|
|
277
|
-
items: [],
|
|
278
|
-
});
|
|
279
|
-
},
|
|
280
|
-
selectIndex: (index: number) => {
|
|
281
367
|
if (selectionMode === ListSelectMode.Single) {
|
|
282
|
-
|
|
368
|
+
// Single selection only follows clicks, so it raises no event of its
|
|
369
|
+
// own, and raising `onItemClicked` would report a click that never
|
|
370
|
+
// happened. The row is just marked.
|
|
371
|
+
applySelection({
|
|
372
|
+
keys: new Set([getRowKey(item.data, index)]),
|
|
373
|
+
isAll: false,
|
|
374
|
+
});
|
|
283
375
|
} else {
|
|
284
|
-
itemSelectedHandler(true, index);
|
|
376
|
+
itemSelectedHandler(true, getRowKey(item.data, index));
|
|
285
377
|
}
|
|
286
378
|
},
|
|
287
379
|
}));
|
|
288
380
|
|
|
381
|
+
const previousSelectionMode = useRef<ListSelectMode>(selectionMode);
|
|
382
|
+
|
|
289
383
|
useEffect(() => {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
384
|
+
const modeChanged = previousSelectionMode.current !== selectionMode;
|
|
385
|
+
previousSelectionMode.current = selectionMode;
|
|
386
|
+
|
|
387
|
+
// Leaving multi select drops the selection, so that the consumer is not
|
|
388
|
+
// left holding a selection that is no longer visible in the list.
|
|
389
|
+
if (modeChanged && selectionMode !== ListSelectMode.Multi) {
|
|
390
|
+
clearSelection();
|
|
297
391
|
}
|
|
298
|
-
}, [selectionMode]);
|
|
392
|
+
}, [clearSelection, selectionMode]);
|
|
299
393
|
|
|
300
394
|
const getRowDisabledState = useCallback(
|
|
301
395
|
(item: ListItem<T>, index: number): boolean => {
|
|
302
396
|
const isDisabledDueToSelectAll =
|
|
303
|
-
|
|
397
|
+
selection.isAll &&
|
|
304
398
|
!enableSelectAllDeselect &&
|
|
305
399
|
selectionMode === ListSelectMode.Multi;
|
|
306
400
|
|
|
@@ -309,95 +403,126 @@ const ListRenderer = <T extends Data>(
|
|
|
309
403
|
|
|
310
404
|
return isDisabledDueToSelectAll || isDisabledByCustomLogic;
|
|
311
405
|
},
|
|
312
|
-
[enableSelectAllDeselect,
|
|
406
|
+
[enableSelectAllDeselect, isRowDisabled, selection.isAll, selectionMode],
|
|
313
407
|
);
|
|
314
408
|
|
|
409
|
+
const triggerIndex = getTriggerIndex(listItems.length, loadingTriggerOffset);
|
|
410
|
+
|
|
411
|
+
// Without a label and without actions there is nothing for the bar to show,
|
|
412
|
+
// so it gives its space back to the list.
|
|
413
|
+
const showActionBar =
|
|
414
|
+
actionBar !== undefined &&
|
|
415
|
+
(!!actionBar.label || actionBar.actions.length > 0);
|
|
416
|
+
|
|
315
417
|
return (
|
|
316
418
|
<div
|
|
317
419
|
className={clsx(classes.wrapper, 'list-wrapper', className)}
|
|
318
|
-
style={customStyles}
|
|
319
420
|
data-test-id="list"
|
|
320
421
|
>
|
|
321
|
-
{/*
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
columnGap={columnGap}
|
|
328
|
-
rowHeight={headerRowHeight}
|
|
329
|
-
actionSize={headerRowActionSize}
|
|
330
|
-
horizontalTextAlign={horizontalTextAlign}
|
|
331
|
-
verticalTextAlign={verticalTextAlign}
|
|
332
|
-
showItemCheckbox={
|
|
333
|
-
enableSelectAll &&
|
|
334
|
-
selectionMode === ListSelectMode.Multi &&
|
|
335
|
-
listItems.length > 0
|
|
336
|
-
}
|
|
337
|
-
isCheckboxDisabled={listItems.length === 0}
|
|
338
|
-
onCheckboxToggled={headerCheckboxHandler}
|
|
339
|
-
onSortChanged={sortChangedHandler}
|
|
340
|
-
onResetColumnSizes={resetColumnSizes}
|
|
341
|
-
onColumnSizesChanged={setColumnSizes}
|
|
342
|
-
hasActionColumn={hasActionColumn}
|
|
343
|
-
/>
|
|
344
|
-
|
|
345
|
-
{/* Rows */}
|
|
346
|
-
{listItems.map((item: ListItem<T>, index) => (
|
|
347
|
-
<ListRowRenderer<T>
|
|
348
|
-
key={String(item.data[keyProperty] || index)}
|
|
349
|
-
customRowRenderer={customRowRenderer}
|
|
350
|
-
isTrigger={isTrigger<T>(index, listItems, loadingTriggerOffset)}
|
|
351
|
-
onTriggered={onTriggeredHandler}
|
|
352
|
-
listRowProps={{
|
|
353
|
-
columns,
|
|
354
|
-
data: item.data,
|
|
355
|
-
itemSelected: item.selected,
|
|
356
|
-
isRowDisabled: getRowDisabledState(item, index),
|
|
357
|
-
columnSizes,
|
|
358
|
-
columnGap,
|
|
359
|
-
rowHeight: listRowHeight,
|
|
360
|
-
actionSize: listRowActionSize,
|
|
361
|
-
horizontalTextAlign,
|
|
362
|
-
verticalTextAlign,
|
|
363
|
-
textWrap,
|
|
364
|
-
selectionMode,
|
|
365
|
-
showActionButton:
|
|
366
|
-
selectionMode === ListSelectMode.None &&
|
|
367
|
-
getActionButtonVisibility(item.data, showActionButton),
|
|
368
|
-
showCheckMark: selectionMode === ListSelectMode.Single,
|
|
369
|
-
showItemCheckbox: selectionMode === ListSelectMode.Multi,
|
|
370
|
-
onItemClicked: generateItemLink
|
|
371
|
-
? generateItemLink(item.data)
|
|
372
|
-
: (data: T) => itemClickedHandler(data, index),
|
|
373
|
-
onItemSelected: (checked: boolean) =>
|
|
374
|
-
itemSelectedHandler(checked, index),
|
|
375
|
-
inlineMenuActions,
|
|
376
|
-
}}
|
|
422
|
+
{/* Actions of the active context, on top of the column header */}
|
|
423
|
+
{showActionBar && actionBar !== undefined && (
|
|
424
|
+
<ActionBar
|
|
425
|
+
{...actionBar}
|
|
426
|
+
height={headerRowHeight}
|
|
427
|
+
className={classes.actionBar}
|
|
377
428
|
/>
|
|
378
|
-
)
|
|
429
|
+
)}
|
|
379
430
|
|
|
380
|
-
{
|
|
381
|
-
|
|
431
|
+
<div className={clsx(classes.list, 'list-container')} style={listStyles}>
|
|
432
|
+
{/* Header */}
|
|
433
|
+
<ListHeader
|
|
434
|
+
columns={columns}
|
|
435
|
+
itemSelected={selection.isAll}
|
|
436
|
+
sortData={sort}
|
|
382
437
|
columnSizes={columnSizes}
|
|
383
438
|
columnGap={columnGap}
|
|
384
|
-
rowHeight={
|
|
385
|
-
|
|
439
|
+
rowHeight={headerRowHeight}
|
|
440
|
+
actionSize={headerRowActionSize}
|
|
441
|
+
actionSlotSize={actionSlotSize}
|
|
442
|
+
// The bar sticks to the top of the list first, so the header comes to
|
|
443
|
+
// rest below it. Its bottom border adds to the height of its row.
|
|
444
|
+
stickyOffset={
|
|
445
|
+
showActionBar ? `calc(${headerRowHeight} + 1px)` : undefined
|
|
446
|
+
}
|
|
447
|
+
horizontalTextAlign={horizontalTextAlign}
|
|
448
|
+
verticalTextAlign={verticalTextAlign}
|
|
449
|
+
showItemCheckbox={
|
|
450
|
+
enableSelectAll &&
|
|
451
|
+
selectionMode === ListSelectMode.Multi &&
|
|
452
|
+
listItems.length > 0
|
|
453
|
+
}
|
|
454
|
+
isCheckboxDisabled={listItems.length === 0}
|
|
455
|
+
onCheckboxToggled={headerCheckboxHandler}
|
|
456
|
+
onSortChanged={sortChangedHandler}
|
|
457
|
+
onResetColumnSizes={resetColumnSizes}
|
|
458
|
+
onColumnSizesChanged={setColumnSizes}
|
|
459
|
+
hasActionColumn={hasActionColumn}
|
|
386
460
|
/>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
461
|
+
|
|
462
|
+
{/* Rows */}
|
|
463
|
+
{listItems.map((item: ListItem<T>, index) => {
|
|
464
|
+
const rowKey = getRowKey(item.data, index);
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<ListRowRenderer<T>
|
|
468
|
+
key={rowKey}
|
|
469
|
+
customRowRenderer={customRowRenderer}
|
|
470
|
+
isTrigger={index === triggerIndex}
|
|
471
|
+
onTriggered={onTriggeredHandler}
|
|
472
|
+
listRowProps={{
|
|
473
|
+
columns,
|
|
474
|
+
data: item.data,
|
|
475
|
+
itemSelected: item.selected,
|
|
476
|
+
isRowDisabled: getRowDisabledState(item, index),
|
|
477
|
+
columnSizes,
|
|
478
|
+
columnGap,
|
|
479
|
+
rowHeight: listRowHeight,
|
|
480
|
+
actionSize: listRowActionSize,
|
|
481
|
+
horizontalTextAlign,
|
|
482
|
+
verticalTextAlign,
|
|
483
|
+
textWrap,
|
|
484
|
+
selectionMode,
|
|
485
|
+
showActionButton:
|
|
486
|
+
selectionMode === ListSelectMode.None &&
|
|
487
|
+
getActionButtonVisibility(item.data, showActionButton),
|
|
488
|
+
showCheckMark: selectionMode === ListSelectMode.Single,
|
|
489
|
+
showItemCheckbox: selectionMode === ListSelectMode.Multi,
|
|
490
|
+
linkTo: generateItemLink?.(item.data),
|
|
491
|
+
onItemClicked: (row: T) => itemClickedHandler(row, index),
|
|
492
|
+
onItemSelected: (checked: boolean) =>
|
|
493
|
+
itemSelectedHandler(checked, rowKey),
|
|
494
|
+
inlineMenuActions,
|
|
495
|
+
}}
|
|
496
|
+
/>
|
|
497
|
+
);
|
|
498
|
+
})}
|
|
499
|
+
|
|
500
|
+
{isLoading && (
|
|
501
|
+
<ListRowLoader
|
|
502
|
+
columnSizes={columnSizes}
|
|
503
|
+
columnGap={columnGap}
|
|
504
|
+
rowHeight={listRowHeight}
|
|
505
|
+
columns={columns}
|
|
398
506
|
/>
|
|
399
|
-
|
|
400
|
-
|
|
507
|
+
)}
|
|
508
|
+
{!isLoading && !isError && listItems.length === 0 && (
|
|
509
|
+
<div className={classes.noData} data-test-id="list-empty">
|
|
510
|
+
<p>No items found</p>
|
|
511
|
+
</div>
|
|
512
|
+
)}
|
|
513
|
+
{isError && handleRetry && (
|
|
514
|
+
<div className={classes.error}>
|
|
515
|
+
<p>{errorMsg}</p>
|
|
516
|
+
<TextButton
|
|
517
|
+
text={'Retry'}
|
|
518
|
+
height={listRowActionSize}
|
|
519
|
+
width={'100px'}
|
|
520
|
+
buttonContext={ButtonContext.Context}
|
|
521
|
+
onButtonClicked={() => onRetry()}
|
|
522
|
+
/>
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
</div>
|
|
401
526
|
</div>
|
|
402
527
|
);
|
|
403
528
|
};
|
|
@@ -412,17 +537,3 @@ const ListRenderer = <T extends Data>(
|
|
|
412
537
|
* />
|
|
413
538
|
*/
|
|
414
539
|
export const List = React.forwardRef(ListRenderer);
|
|
415
|
-
|
|
416
|
-
const noItemsMessage = (
|
|
417
|
-
itemsCount: number,
|
|
418
|
-
isLoading: boolean,
|
|
419
|
-
isError: boolean,
|
|
420
|
-
): ReactElement | undefined => {
|
|
421
|
-
if (!isLoading && !isError && itemsCount === 0) {
|
|
422
|
-
return (
|
|
423
|
-
<div className={clsx(classes.NoData)} data-test-id="list-empty">
|
|
424
|
-
<p>No items found</p>
|
|
425
|
-
</div>
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
border-bottom: solid 1px
|
|
16
16
|
var(--list-header-border-color, $list-header-border-color);
|
|
17
17
|
|
|
18
|
+
// The select all checkbox lines up with the checkboxes of the rows, which
|
|
19
|
+
// occupy the last slot of the actions column - any slot before it belongs to
|
|
20
|
+
// the inline menu or the row action button.
|
|
21
|
+
.actionColumn {
|
|
22
|
+
justify-items: end;
|
|
23
|
+
|
|
24
|
+
.selectAllSlot {
|
|
25
|
+
height: 100%;
|
|
26
|
+
|
|
27
|
+
display: grid;
|
|
28
|
+
place-items: center;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
.columnLabel {
|
|
19
33
|
box-sizing: border-box;
|
|
20
34
|
width: 100%;
|