@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
|
@@ -18,6 +18,13 @@ export interface ListHeaderProps<T extends Data> {
|
|
|
18
18
|
rowHeight: string;
|
|
19
19
|
/** Size of the checkbox */
|
|
20
20
|
actionSize: string;
|
|
21
|
+
/** Width of a single action slot of the trailing actions column */
|
|
22
|
+
actionSlotSize: string;
|
|
23
|
+
/**
|
|
24
|
+
* Distance the header keeps from the top of the list while it is stuck, to
|
|
25
|
+
* leave room for whatever sticks above it. (default: '0')
|
|
26
|
+
*/
|
|
27
|
+
stickyOffset?: string;
|
|
21
28
|
/** Horizontal alignment of text */
|
|
22
29
|
horizontalTextAlign: 'left' | 'right' | 'center';
|
|
23
30
|
/** Vertical alignment of text */
|
|
@@ -54,6 +61,8 @@ export interface ListHeaderProps<T extends Data> {
|
|
|
54
61
|
* columnSizes={'1fr'}
|
|
55
62
|
* columnGap={'5px'}
|
|
56
63
|
* rowHeight={'44px'}
|
|
64
|
+
* actionSize={'28px'}
|
|
65
|
+
* actionSlotSize={'50px'}
|
|
57
66
|
* horizontalTextAlign={'left'}
|
|
58
67
|
* verticalTextAlign={'center'}
|
|
59
68
|
* />
|
|
@@ -64,6 +73,8 @@ export const ListHeader = <T extends Data>({
|
|
|
64
73
|
columnGap,
|
|
65
74
|
rowHeight,
|
|
66
75
|
actionSize,
|
|
76
|
+
actionSlotSize,
|
|
77
|
+
stickyOffset,
|
|
67
78
|
horizontalTextAlign,
|
|
68
79
|
verticalTextAlign,
|
|
69
80
|
itemSelected = false,
|
|
@@ -83,6 +94,7 @@ export const ListHeader = <T extends Data>({
|
|
|
83
94
|
gridColumnGap: columnGap,
|
|
84
95
|
justifyItems: horizontalTextAlign,
|
|
85
96
|
alignItems: verticalTextAlign,
|
|
97
|
+
top: stickyOffset,
|
|
86
98
|
} as React.CSSProperties;
|
|
87
99
|
|
|
88
100
|
const { cols, mouseDown, ResizeIndicator } = useResize(
|
|
@@ -131,16 +143,21 @@ export const ListHeader = <T extends Data>({
|
|
|
131
143
|
{hasActionColumn && (
|
|
132
144
|
<div
|
|
133
145
|
ref={cols[cols.length - 1].ref}
|
|
134
|
-
className={clsx(classes.columnLabel)}
|
|
146
|
+
className={clsx(classes.columnLabel, classes.actionColumn)}
|
|
135
147
|
>
|
|
136
148
|
{showItemCheckbox && (
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
<div
|
|
150
|
+
className={classes.selectAllSlot}
|
|
151
|
+
style={{ width: actionSlotSize }}
|
|
152
|
+
>
|
|
153
|
+
<ListCheckBox
|
|
154
|
+
height={actionSize}
|
|
155
|
+
width={actionSize}
|
|
156
|
+
isChecked={itemSelected}
|
|
157
|
+
isDisabled={isCheckboxDisabled}
|
|
158
|
+
onCheckBoxToggled={onCheckboxToggled}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
144
161
|
)}
|
|
145
162
|
</div>
|
|
146
163
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
2
|
import { fireEvent, render } from '@testing-library/react';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import { BrowserRouter, MemoryRouter, useLocation } from 'react-router-dom';
|
|
5
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { type Column, ListSelectMode } from '../List.model';
|
|
7
7
|
import { ListRow, type ListRowProps, setLocale } from './ListRow';
|
|
@@ -401,50 +401,137 @@ describe('ListRow', () => {
|
|
|
401
401
|
});
|
|
402
402
|
|
|
403
403
|
describe('linking', () => {
|
|
404
|
-
|
|
404
|
+
/** Renders the path the router currently points at, to tell a click that
|
|
405
|
+
* navigated apart from one that was suppressed. `Link` calls
|
|
406
|
+
* `preventDefault` itself whenever it navigates, so the event is no help. */
|
|
407
|
+
const LocationProbe: React.FC = () => {
|
|
408
|
+
const { pathname } = useLocation();
|
|
409
|
+
return <span data-test-id="location">{pathname}</span>;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const renderRow = (
|
|
413
|
+
props: Partial<ListRowProps<TestExplorerData>>,
|
|
414
|
+
): { link: HTMLAnchorElement; path: () => string | null } => {
|
|
415
|
+
const { container } = render(
|
|
416
|
+
<MemoryRouter initialEntries={['/start']}>
|
|
417
|
+
<ListRow {...mockProps} {...props} />
|
|
418
|
+
<LocationProbe />
|
|
419
|
+
</MemoryRouter>,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
link: container.querySelector('a') as HTMLAnchorElement,
|
|
424
|
+
path: () =>
|
|
425
|
+
container.querySelector('[data-test-id="location"]')?.textContent ??
|
|
426
|
+
null,
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
it(`does render a link if it got a 'linkTo'`, () => {
|
|
405
431
|
const { container } = render(
|
|
406
432
|
<BrowserRouter>
|
|
407
433
|
<ListRow
|
|
408
434
|
{...mockProps}
|
|
409
|
-
|
|
435
|
+
linkTo="http://this.is.a.test.url"
|
|
410
436
|
selectionMode={ListSelectMode.None}
|
|
411
437
|
/>
|
|
412
438
|
</BrowserRouter>,
|
|
413
439
|
);
|
|
414
440
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
expect(link).toBeInTheDocument();
|
|
441
|
+
expect(container.querySelector('a')).toBeInTheDocument();
|
|
418
442
|
});
|
|
419
443
|
|
|
420
|
-
it(
|
|
444
|
+
it(`does not render a link if it got no 'linkTo'`, () => {
|
|
421
445
|
const { container } = render(
|
|
422
446
|
<ListRow
|
|
423
447
|
{...mockProps}
|
|
424
|
-
onItemClicked=
|
|
448
|
+
onItemClicked={() => {
|
|
449
|
+
// empty
|
|
450
|
+
}}
|
|
425
451
|
selectionMode={ListSelectMode.Single}
|
|
426
452
|
/>,
|
|
427
453
|
);
|
|
428
454
|
|
|
429
|
-
|
|
455
|
+
expect(container.querySelector('.link')).not.toBeInTheDocument();
|
|
456
|
+
});
|
|
430
457
|
|
|
431
|
-
|
|
458
|
+
it('navigates when there is no selection mode', () => {
|
|
459
|
+
const onItemClickedSpy = vi.fn();
|
|
460
|
+
const { link, path } = renderRow({
|
|
461
|
+
linkTo: '/somewhere',
|
|
462
|
+
selectionMode: ListSelectMode.None,
|
|
463
|
+
onItemClicked: onItemClickedSpy,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
fireEvent.click(link);
|
|
467
|
+
|
|
468
|
+
expect(path()).toBe('/somewhere');
|
|
469
|
+
// The link takes precedence, so the click handler stays out of the way
|
|
470
|
+
expect(onItemClickedSpy).not.toHaveBeenCalled();
|
|
432
471
|
});
|
|
433
472
|
|
|
434
|
-
it('
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
);
|
|
473
|
+
it('keeps the link in the tree in single select mode, but does not navigate', () => {
|
|
474
|
+
const onItemClickedSpy = vi.fn();
|
|
475
|
+
const { link, path } = renderRow({
|
|
476
|
+
linkTo: '/somewhere',
|
|
477
|
+
selectionMode: ListSelectMode.Single,
|
|
478
|
+
onItemClicked: onItemClickedSpy,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// The anchor stays, so that switching selection mode does not remount the cells
|
|
482
|
+
expect(link).toBeInTheDocument();
|
|
483
|
+
|
|
484
|
+
fireEvent.click(link);
|
|
485
|
+
|
|
486
|
+
expect(path()).toBe('/start');
|
|
487
|
+
expect(onItemClickedSpy).toHaveBeenCalledTimes(1);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('selects instead of navigating in multi select mode', () => {
|
|
491
|
+
const onItemSelectedSpy = vi.fn();
|
|
492
|
+
const { link, path } = renderRow({
|
|
493
|
+
linkTo: '/somewhere',
|
|
494
|
+
selectionMode: ListSelectMode.Multi,
|
|
495
|
+
itemSelected: false,
|
|
496
|
+
onItemSelected: onItemSelectedSpy,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
fireEvent.click(link);
|
|
444
500
|
|
|
445
|
-
|
|
501
|
+
expect(path()).toBe('/start');
|
|
502
|
+
expect(onItemSelectedSpy).toHaveBeenCalledWith(true);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('does not navigate if the row is disabled', () => {
|
|
506
|
+
const onItemClickedSpy = vi.fn();
|
|
507
|
+
const { link, path } = renderRow({
|
|
508
|
+
linkTo: '/somewhere',
|
|
509
|
+
selectionMode: ListSelectMode.None,
|
|
510
|
+
isRowDisabled: true,
|
|
511
|
+
onItemClicked: onItemClickedSpy,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
fireEvent.click(link);
|
|
515
|
+
|
|
516
|
+
expect(path()).toBe('/start');
|
|
517
|
+
expect(onItemClickedSpy).not.toHaveBeenCalled();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('does not navigate on a middle click while the link is suppressed', () => {
|
|
521
|
+
const { link } = renderRow({
|
|
522
|
+
linkTo: '/somewhere',
|
|
523
|
+
selectionMode: ListSelectMode.Multi,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// `Link` ignores auxclick entirely, so a prevented default here is ours
|
|
527
|
+
const event = new MouseEvent('auxclick', {
|
|
528
|
+
bubbles: true,
|
|
529
|
+
cancelable: true,
|
|
530
|
+
button: 1,
|
|
531
|
+
});
|
|
532
|
+
fireEvent(link, event);
|
|
446
533
|
|
|
447
|
-
expect(
|
|
534
|
+
expect(event.defaultPrevented).toBe(true);
|
|
448
535
|
});
|
|
449
536
|
});
|
|
450
537
|
|
|
@@ -44,11 +44,22 @@ export interface ListRowProps<T extends Data> {
|
|
|
44
44
|
showItemCheckbox?: boolean;
|
|
45
45
|
/** Defines whether a row is disabled (default: false) */
|
|
46
46
|
isRowDisabled?: boolean;
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Location the user should be navigated to when the row is clicked. A row with
|
|
49
|
+
* a link renders its content as an anchor, which is preferred over routing in
|
|
50
|
+
* JavaScript when the row leads to e.g. a details page.
|
|
51
|
+
*
|
|
52
|
+
* Whether this is set decides the element the row content is wrapped in, so it
|
|
53
|
+
* must not come and go over the lifetime of a row: doing so remounts every
|
|
54
|
+
* cell and resets their state. To stop a row from navigating, put the list in
|
|
55
|
+
* a selection mode or disable the row instead of removing the link.
|
|
56
|
+
*/
|
|
57
|
+
linkTo?: LocationDescriptor<unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* Called when the row is clicked. A row that navigates through `linkTo` only
|
|
60
|
+
* raises this while its navigation is suppressed.
|
|
61
|
+
*/
|
|
62
|
+
onItemClicked?: (data: T) => void;
|
|
52
63
|
/**
|
|
53
64
|
* A function that is getting called, when the selection state of the row changes.
|
|
54
65
|
*/
|
|
@@ -105,6 +116,7 @@ const ListRowComponent = <T extends Data>(
|
|
|
105
116
|
showActionButton = true,
|
|
106
117
|
showCheckMark = false,
|
|
107
118
|
showItemCheckbox = false,
|
|
119
|
+
linkTo,
|
|
108
120
|
onItemClicked = noop,
|
|
109
121
|
onItemSelected = noop,
|
|
110
122
|
isRowDisabled = false,
|
|
@@ -131,16 +143,41 @@ const ListRowComponent = <T extends Data>(
|
|
|
131
143
|
if (selectionMode === ListSelectMode.Multi) {
|
|
132
144
|
onItemSelected(!itemSelected);
|
|
133
145
|
// Emit onItemClicked for all other modes
|
|
134
|
-
} else
|
|
146
|
+
} else {
|
|
135
147
|
onItemClicked(data);
|
|
136
148
|
}
|
|
137
149
|
},
|
|
138
150
|
[isRowDisabled, itemSelected, onItemClicked, onItemSelected, selectionMode],
|
|
139
151
|
);
|
|
140
152
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
153
|
+
// A row that is disabled, or that is part of a selection, selects instead of
|
|
154
|
+
// navigating. The anchor stays in the tree either way - swapping it for
|
|
155
|
+
// another element would remount every cell and reset their state.
|
|
156
|
+
const suppressNavigation =
|
|
157
|
+
isRowDisabled || selectionMode !== ListSelectMode.None;
|
|
158
|
+
|
|
159
|
+
const onLinkClicked = useCallback(
|
|
160
|
+
(event: React.MouseEvent): void => {
|
|
161
|
+
if (!suppressNavigation) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
onItemClickedHandler(data);
|
|
167
|
+
},
|
|
168
|
+
[data, onItemClickedHandler, suppressNavigation],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// A middle click raises `auxclick` instead of `click`, and would otherwise
|
|
172
|
+
// open the suppressed link in a new tab.
|
|
173
|
+
const onLinkAuxClicked = useCallback(
|
|
174
|
+
(event: React.MouseEvent): void => {
|
|
175
|
+
if (suppressNavigation) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[suppressNavigation],
|
|
180
|
+
);
|
|
144
181
|
|
|
145
182
|
const generateCells = useMemo(() => {
|
|
146
183
|
return columns.map((column: Column<T>) => {
|
|
@@ -174,10 +211,12 @@ const ListRowComponent = <T extends Data>(
|
|
|
174
211
|
data-test-id="list-entry"
|
|
175
212
|
>
|
|
176
213
|
{/* Items */}
|
|
177
|
-
{
|
|
214
|
+
{linkTo !== undefined ? (
|
|
178
215
|
<Link
|
|
179
|
-
to={
|
|
216
|
+
to={linkTo}
|
|
180
217
|
className={clsx(classes.content, classes.link)}
|
|
218
|
+
onClick={onLinkClicked}
|
|
219
|
+
onAuxClick={onLinkAuxClicked}
|
|
181
220
|
>
|
|
182
221
|
{generateCells}
|
|
183
222
|
</Link>
|
|
@@ -206,12 +245,12 @@ const ListRowComponent = <T extends Data>(
|
|
|
206
245
|
{showActionButton && selectionMode === ListSelectMode.None && (
|
|
207
246
|
<>
|
|
208
247
|
{showActionButton &&
|
|
209
|
-
(
|
|
248
|
+
(linkTo !== undefined ? (
|
|
210
249
|
<Button
|
|
211
250
|
icon={IconName.ChevronRight}
|
|
212
251
|
height={actionSize}
|
|
213
252
|
width={actionSize}
|
|
214
|
-
path={
|
|
253
|
+
path={linkTo}
|
|
215
254
|
dataTestId="list-entry-action"
|
|
216
255
|
/>
|
|
217
256
|
) : (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useRef } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import type { Data } from '../../../types/data';
|
|
3
3
|
import { ListRow, type ListRowProps } from '../ListRow/ListRow';
|
|
4
4
|
|
|
@@ -30,31 +30,37 @@ export const ListRowRenderer = <T extends Data>({
|
|
|
30
30
|
> | null => {
|
|
31
31
|
// Pagination trigger logic
|
|
32
32
|
// Trigger based on: https://www.youtube.com/watch?v=NZKUirTtxcg
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const observer = useRef<IntersectionObserver>();
|
|
34
|
+
|
|
35
|
+
// `onTriggered` is read through a ref so that `elementRef` below can stay
|
|
36
|
+
// stable. React only re-runs a callback ref when its identity or the node
|
|
37
|
+
// changes, and every re-run observes the node again - which reports the
|
|
38
|
+
// current intersection state right away and would raise `onTriggered` anew
|
|
39
|
+
// for a row that never moved.
|
|
40
|
+
const onTriggeredRef = useRef(onTriggered);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
onTriggeredRef.current = onTriggered;
|
|
35
44
|
}, [onTriggered]);
|
|
36
45
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
observer.current = new IntersectionObserver(([entry]) => {
|
|
47
|
-
if (entry.isIntersecting) {
|
|
48
|
-
onTriggeredHandler();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
46
|
+
const elementRef = useCallback((node: HTMLDivElement | null): void => {
|
|
47
|
+
observer.current?.disconnect();
|
|
48
|
+
observer.current = undefined;
|
|
49
|
+
|
|
50
|
+
// React passes null when the node is detached, either on unmount or
|
|
51
|
+
// because the row stopped being the trigger.
|
|
52
|
+
if (node === null) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
observer.current = new IntersectionObserver(([entry]) => {
|
|
57
|
+
if (entry.isIntersecting) {
|
|
58
|
+
onTriggeredRef.current?.();
|
|
54
59
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
observer.current.observe(node);
|
|
63
|
+
}, []);
|
|
58
64
|
|
|
59
65
|
if (customRowRenderer) {
|
|
60
66
|
const customElement = customRowRenderer(listRowProps);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getTriggerIndex } from './helpers';
|
|
3
|
+
|
|
4
|
+
describe('getTriggerIndex', () => {
|
|
5
|
+
it('sits loadingTriggerOffset rows before the end', () => {
|
|
6
|
+
// 100 rows, 10 rows left after the trigger row (indices 90-99)
|
|
7
|
+
expect(getTriggerIndex(100, 10)).toBe(89);
|
|
8
|
+
expect(getTriggerIndex(30, 10)).toBe(19);
|
|
9
|
+
expect(getTriggerIndex(11, 10)).toBe(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('clamps to the first row when the page is shorter than the offset', () => {
|
|
13
|
+
// Such a page cannot fill the viewport, so no scrolling could ever bring a
|
|
14
|
+
// later row into view - the list has to ask for more data straight away.
|
|
15
|
+
expect(getTriggerIndex(10, 10)).toBe(0);
|
|
16
|
+
expect(getTriggerIndex(5, 10)).toBe(0);
|
|
17
|
+
expect(getTriggerIndex(1, 10)).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('has no trigger row if there are no rows', () => {
|
|
21
|
+
expect(getTriggerIndex(0, 10)).toBe(-1);
|
|
22
|
+
expect(getTriggerIndex(0, 0)).toBe(-1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('triggers on the last row if the offset is zero or negative', () => {
|
|
26
|
+
expect(getTriggerIndex(30, 0)).toBe(29);
|
|
27
|
+
expect(getTriggerIndex(30, -5)).toBe(29);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
import type { ListItem, SortData } from './List.model';
|
|
2
|
+
import type { SortData } from './List.model';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Index of the row that requests the next page once it comes into view. It sits
|
|
6
|
+
* `loadingTriggerOffset` rows before the end, clamped to the first row: a page
|
|
7
|
+
* that is shorter than the offset does not fill the viewport, so there is no
|
|
8
|
+
* scrolling that could ever bring a later row into view.
|
|
9
|
+
* @param itemCount number of rows currently rendered
|
|
10
|
+
* @param loadingTriggerOffset number of rows left before a load is triggered
|
|
11
|
+
* @returns the index of the trigger row, or -1 if there are no rows
|
|
12
|
+
*/
|
|
13
|
+
export const getTriggerIndex = (
|
|
14
|
+
itemCount: number,
|
|
8
15
|
loadingTriggerOffset: number,
|
|
9
|
-
):
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
): number =>
|
|
17
|
+
itemCount === 0
|
|
18
|
+
? -1
|
|
19
|
+
: Math.max(0, itemCount - Math.max(0, loadingTriggerOffset) - 1);
|
|
12
20
|
|
|
13
21
|
export const useSort = <T>(
|
|
14
22
|
defaultSortOrder: SortData<T> | undefined,
|
|
@@ -21,7 +29,7 @@ export const useSort = <T>(
|
|
|
21
29
|
|
|
22
30
|
const sortChangedHandler = (sort: SortData<T>): void => {
|
|
23
31
|
setSort(sort);
|
|
24
|
-
onSortChanged
|
|
32
|
+
onSortChanged(sort);
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
return { sort, sortChangedHandler } as const;
|
|
@@ -30,24 +30,31 @@ const getColumnsSizeDefinition = function <T extends Data>(
|
|
|
30
30
|
return columnSizeDefinition.join(' ');
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/** Width of a single action within the trailing actions column. */
|
|
34
|
+
const ACTION_SLOT_SIZE_PX = 50;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Spacing between two actions of the trailing actions column. Has to match the
|
|
38
|
+
* `grid-gap` of the actions of ListRow.
|
|
39
|
+
*/
|
|
40
|
+
const ACTION_SLOT_GAP_PX = 8;
|
|
41
|
+
|
|
33
42
|
const getActionsColumnSizePx = (...enableActions: boolean[]): string => {
|
|
34
43
|
const enabledActionsCount = enableActions.filter(
|
|
35
44
|
(actionEnabled) => actionEnabled,
|
|
36
45
|
).length;
|
|
37
46
|
|
|
38
|
-
const defaultActionSizePx = 50;
|
|
39
|
-
const defaultActionsRawGapPx = 8;
|
|
40
47
|
const calculateMultiActionsColumnSizePx = (actionsCount: number): number => {
|
|
41
48
|
return (
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
ACTION_SLOT_SIZE_PX * actionsCount +
|
|
50
|
+
ACTION_SLOT_GAP_PX * (actionsCount - 1)
|
|
44
51
|
);
|
|
45
52
|
};
|
|
46
53
|
|
|
47
54
|
const sizePx =
|
|
48
55
|
enabledActionsCount > 1
|
|
49
56
|
? calculateMultiActionsColumnSizePx(enabledActionsCount)
|
|
50
|
-
:
|
|
57
|
+
: ACTION_SLOT_SIZE_PX;
|
|
51
58
|
|
|
52
59
|
return `${sizePx}px`;
|
|
53
60
|
};
|
|
@@ -59,8 +66,9 @@ const getActionsColumnSizePx = (...enableActions: boolean[]): string => {
|
|
|
59
66
|
* @param selectMode current select mode
|
|
60
67
|
* @param showInlineMenu whether the inline menu is shown
|
|
61
68
|
* @returns an object containing the columnSizes (CSS grid-template-columns),
|
|
62
|
-
* a reset function (to reset to the initial column sizes)
|
|
63
|
-
*
|
|
69
|
+
* a reset function (to reset to the initial column sizes),
|
|
70
|
+
* a setter function (to update the column sizes)
|
|
71
|
+
* and the size of a single action slot of the trailing actions column
|
|
64
72
|
*/
|
|
65
73
|
export const useColumnsSize = <T extends Data>(
|
|
66
74
|
columns: Column<T>[],
|
|
@@ -72,6 +80,7 @@ export const useColumnsSize = <T extends Data>(
|
|
|
72
80
|
resetColumnSizes: () => void;
|
|
73
81
|
setColumnSizes: (newColumnSizes: string) => void;
|
|
74
82
|
hasActionColumn: boolean;
|
|
83
|
+
actionSlotSize: string;
|
|
75
84
|
} => {
|
|
76
85
|
const [isResized, setIsResized] = useState<boolean>(false);
|
|
77
86
|
|
|
@@ -124,6 +133,7 @@ export const useColumnsSize = <T extends Data>(
|
|
|
124
133
|
showActionButton,
|
|
125
134
|
showInlineMenu,
|
|
126
135
|
),
|
|
136
|
+
actionSlotSize: `${ACTION_SLOT_SIZE_PX}px`,
|
|
127
137
|
};
|
|
128
138
|
};
|
|
129
139
|
function hasActionColumn(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { IconName } from '../Icons';
|
|
1
2
|
import type { PageHeaderActionProps } from './PageHeaderAction/PageHeaderAction.model';
|
|
2
|
-
import type { PageHeaderActionsGroupProps } from './PageHeaderActionsGroup/PageHeaderActionsGroup';
|
|
3
3
|
|
|
4
4
|
export interface PageHeaderProps {
|
|
5
5
|
/** Title shown in page header */
|
|
@@ -17,6 +17,43 @@ export interface PageHeaderProps {
|
|
|
17
17
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
18
18
|
export interface PageHeaderSpacerProps {}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A group of actions. The group renders a single action on the page header,
|
|
22
|
+
* which opens an action bar holding the actions of the group below the page
|
|
23
|
+
* header. Only one group can be open at a time, and groups are told apart by
|
|
24
|
+
* their label.
|
|
25
|
+
*/
|
|
26
|
+
export interface PageHeaderActionsGroupProps {
|
|
27
|
+
/** Array of Actions to be rendered on the action bar */
|
|
28
|
+
actions: PageHeaderActionProps[];
|
|
29
|
+
/** Whether group is disabled (default: false) */
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** Whether actions in the group are disabled or not (default: false)*/
|
|
32
|
+
groupActionsDisabled?: boolean;
|
|
33
|
+
/** CSS Class name for additional styles of the action that opens the group */
|
|
34
|
+
className?: string;
|
|
35
|
+
/** The label of the action. */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Optional built in icon. This prop also accepts an img src. */
|
|
38
|
+
icon?: IconName | string;
|
|
39
|
+
/**
|
|
40
|
+
* Optional text rendered at the start of the action bar while the group is
|
|
41
|
+
* open, e.g. 'Selected 3'. (default: undefined)
|
|
42
|
+
*/
|
|
43
|
+
actionBarLabel?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Whether group actions are shown by default. Only one group can be open, so
|
|
46
|
+
* if several groups set this, the first of them opens. (default: false)
|
|
47
|
+
*/
|
|
48
|
+
openActionsGroupOnStart?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Callback to emit when Group Actions is toggled
|
|
51
|
+
* The expanded state is supplied as an argument
|
|
52
|
+
* A rejected promise aborts the toggle.
|
|
53
|
+
*/
|
|
54
|
+
onActionsGroupToggled?: (expanded: boolean) => Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
20
57
|
export type PageHeaderActionItemProps =
|
|
21
58
|
| (PageHeaderSpacerProps & { kind: 'spacer' })
|
|
22
59
|
| (PageHeaderActionProps & { kind: 'action' })
|