@axinom/mosaic-ui 0.49.0-rc.8 → 0.49.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/components/DynamicDataList/DynamicDataList.d.ts +3 -1
- package/dist/components/DynamicDataList/DynamicDataList.d.ts.map +1 -1
- package/dist/components/DynamicDataList/DynamicListRow/DynamicListRow.d.ts +3 -1
- package/dist/components/DynamicDataList/DynamicListRow/DynamicListRow.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.d.ts +2 -0
- package/dist/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/components/Filters/Filter/Filter.d.ts +2 -1
- package/dist/components/Filters/Filter/Filter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.d.ts.map +1 -1
- package/dist/components/List/List.d.ts +2 -0
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/ListRow/ListRow.d.ts +4 -2
- package/dist/components/List/ListRow/ListRow.d.ts.map +1 -1
- package/dist/components/List/ListRow/ListRowLoader.d.ts +1 -1
- package/dist/components/List/ListRow/ListRowLoader.d.ts.map +1 -1
- package/dist/index.es.js +5 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/DynamicDataList/DynamicDataList.stories.tsx +2 -1
- package/src/components/DynamicDataList/DynamicDataList.tsx +4 -0
- package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.scss +11 -5
- package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.spec.tsx +37 -0
- package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.tsx +23 -14
- package/src/components/Explorer/Explorer.stories.tsx +1 -0
- package/src/components/Explorer/Explorer.tsx +5 -0
- package/src/components/Filters/Filter/Filter.tsx +3 -0
- package/src/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.spec.tsx +16 -1
- package/src/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.tsx +13 -1
- package/src/components/List/List.stories.tsx +2 -1
- package/src/components/List/List.tsx +5 -1
- package/src/components/List/ListRow/ListRow.scss +11 -4
- package/src/components/List/ListRow/ListRow.spec.tsx +35 -0
- package/src/components/List/ListRow/ListRow.tsx +44 -17
- package/src/components/List/ListRow/ListRowLoader.tsx +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axinom/mosaic-ui",
|
|
3
|
-
"version": "0.49.0
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "UI components for building Axinom Mosaic applications",
|
|
5
5
|
"author": "Axinom",
|
|
6
6
|
"license": "PROPRIETARY",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"build-storybook": "storybook build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@axinom/mosaic-core": "^0.4.22
|
|
35
|
+
"@axinom/mosaic-core": "^0.4.22",
|
|
36
36
|
"@faker-js/faker": "^7.4.0",
|
|
37
37
|
"@popperjs/core": "^2.11.8",
|
|
38
38
|
"clsx": "^1.1.0",
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"publishConfig": {
|
|
106
106
|
"access": "public"
|
|
107
107
|
},
|
|
108
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "a013c09e64dbd8e829e6da59048ccf7ed56d0eac"
|
|
109
109
|
}
|
|
@@ -44,7 +44,7 @@ const generateData = (amount: number): DynamicListStoryData[] =>
|
|
|
44
44
|
position: amount - index, // Position and ID is jumbled for demonstration purposes
|
|
45
45
|
id: index,
|
|
46
46
|
desc: `Description ${index}: ${faker.lorem.words(
|
|
47
|
-
faker.datatype.number({ min: 10, max:
|
|
47
|
+
faker.datatype.number({ min: 10, max: 50 }),
|
|
48
48
|
)}`,
|
|
49
49
|
title: `Item ${index}: ${faker.random.words(
|
|
50
50
|
faker.datatype.number({ min: 1, max: 3 }),
|
|
@@ -87,6 +87,7 @@ const groups = createGroups({
|
|
|
87
87
|
'headerRowActionSize',
|
|
88
88
|
'horizontalTextAlign',
|
|
89
89
|
'verticalTextAlign',
|
|
90
|
+
'textWrap',
|
|
90
91
|
'rowClassNameProvider',
|
|
91
92
|
],
|
|
92
93
|
});
|
|
@@ -53,6 +53,8 @@ export interface DynamicDataListProps<T extends Data> {
|
|
|
53
53
|
horizontalTextAlign?: 'left' | 'right' | 'center';
|
|
54
54
|
/** Vertical alignment of text */
|
|
55
55
|
verticalTextAlign?: 'start' | 'center' | 'end';
|
|
56
|
+
/** If set to true, column text overflow will be wrapped to a new line. Otherwise, it will be truncated with an ellipsis (default: false) */
|
|
57
|
+
textWrap?: boolean;
|
|
56
58
|
/** Property that contains the value used in reordering the list (default: undefined) */
|
|
57
59
|
positionPropertyName?: keyof T;
|
|
58
60
|
/** If sets, sets the label for the position column (default: 'Position') */
|
|
@@ -127,6 +129,7 @@ export const DynamicDataList = <T extends Data>({
|
|
|
127
129
|
stickyHeader = true,
|
|
128
130
|
disabled = false,
|
|
129
131
|
className = '',
|
|
132
|
+
textWrap = false,
|
|
130
133
|
onChange = noop,
|
|
131
134
|
onAddTransformData = (data) => data as T,
|
|
132
135
|
rowClassNameProvider,
|
|
@@ -233,6 +236,7 @@ export const DynamicDataList = <T extends Data>({
|
|
|
233
236
|
actionSize={listRowActionSize}
|
|
234
237
|
horizontalTextAlign={horizontalTextAlign}
|
|
235
238
|
verticalTextAlign={verticalTextAlign}
|
|
239
|
+
textWrap={textWrap}
|
|
236
240
|
allowRemove={allowNewData}
|
|
237
241
|
positionKey={positionPropertyName}
|
|
238
242
|
allowDragging={allowRowDragging}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
display: grid;
|
|
11
11
|
padding: 1px 0px 1px 0px;
|
|
12
|
-
grid-auto-rows: var(--dynamic-list-row-height, $dynamic-list-row-height);
|
|
13
12
|
column-gap: var(--dynamic-list-column-gap, $dynamic-list-column-gap);
|
|
14
13
|
|
|
15
14
|
border-bottom: var(--dynamic-list-row-border, $dynamic-list-row-border);
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
align-items: center;
|
|
27
26
|
|
|
28
27
|
.wrapper {
|
|
29
|
-
min-height:
|
|
28
|
+
min-height: var(--dynamic-list-row-height, $dynamic-list-row-height);
|
|
30
29
|
min-width: 100%;
|
|
31
30
|
display: grid;
|
|
32
31
|
align-items: center;
|
|
@@ -41,9 +40,16 @@
|
|
|
41
40
|
max-width: 100%;
|
|
42
41
|
max-height: 100%;
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
span {
|
|
44
|
+
padding: 5px 0;
|
|
45
|
+
display: grid;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&.nowrap {
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
text-overflow: ellipsis;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
.position,
|
|
@@ -407,6 +407,43 @@ describe('DynamicListRow', () => {
|
|
|
407
407
|
expect(input.prop('value')).toBe(dataWithPosition.position);
|
|
408
408
|
});
|
|
409
409
|
|
|
410
|
+
describe('DynamicListRow column text alignments', () => {
|
|
411
|
+
const alignments: {
|
|
412
|
+
horizontal: 'left' | 'right' | 'center' | undefined;
|
|
413
|
+
vertical: 'center' | 'start' | 'end' | undefined;
|
|
414
|
+
}[] = [
|
|
415
|
+
{ horizontal: 'center', vertical: 'center' },
|
|
416
|
+
{ horizontal: 'left', vertical: 'start' },
|
|
417
|
+
{ horizontal: 'right', vertical: 'end' },
|
|
418
|
+
{ horizontal: undefined, vertical: undefined },
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
alignments.forEach(({ horizontal, vertical }) => {
|
|
422
|
+
it(`should apply the correct styles for justify-content: ${horizontal}, align-items: ${vertical}`, () => {
|
|
423
|
+
const wrapper = mount(
|
|
424
|
+
<DynamicListRow
|
|
425
|
+
columns={defaultColumns}
|
|
426
|
+
columnSizes={defaultProps.columnSizes}
|
|
427
|
+
data={dataWithPosition}
|
|
428
|
+
positionKey={'position'}
|
|
429
|
+
showPositionColumn={true}
|
|
430
|
+
horizontalTextAlign={horizontal}
|
|
431
|
+
verticalTextAlign={vertical}
|
|
432
|
+
/>,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const wrapperDivs = wrapper.find('.wrapper');
|
|
436
|
+
|
|
437
|
+
wrapperDivs.forEach((node) => {
|
|
438
|
+
const style = node.prop('style');
|
|
439
|
+
|
|
440
|
+
expect(style).toHaveProperty('justifyContent', horizontal);
|
|
441
|
+
expect(style).toHaveProperty('alignItems', vertical);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
410
447
|
describe('tooltip', () => {
|
|
411
448
|
it(`renders a tooltip using the 'title' html attribute by default`, () => {
|
|
412
449
|
const wrapper = shallow(
|
|
@@ -28,6 +28,8 @@ export interface DynamicListRowProps<T extends Data> {
|
|
|
28
28
|
horizontalTextAlign?: 'left' | 'right' | 'center';
|
|
29
29
|
/** Vertical alignment of text */
|
|
30
30
|
verticalTextAlign?: 'start' | 'center' | 'end';
|
|
31
|
+
/** If set to true, column text overflow will be wrapped to a new line. Otherwise, it will be truncated with an ellipsis (default: false) */
|
|
32
|
+
textWrap?: boolean;
|
|
31
33
|
/** If set to true, the remove action button will be rendered (default: undefined) */
|
|
32
34
|
allowRemove?: boolean;
|
|
33
35
|
/** If set to true, editable fields will be highlighted and row click events will be fired (default: false) */
|
|
@@ -85,6 +87,7 @@ export const DynamicListRow = <T extends Data>({
|
|
|
85
87
|
showPositionColumn = false,
|
|
86
88
|
showActionColumn = false,
|
|
87
89
|
allowEditing = false,
|
|
90
|
+
textWrap = false,
|
|
88
91
|
}: PropsWithChildren<DynamicListRowProps<T>>): JSX.Element => {
|
|
89
92
|
const customStyles = {
|
|
90
93
|
gridAutoRows: `minmax(50px, ${rowHeight})`,
|
|
@@ -170,7 +173,7 @@ export const DynamicListRow = <T extends Data>({
|
|
|
170
173
|
</div>
|
|
171
174
|
)}
|
|
172
175
|
{columns.map((column: DynamicListColumn<T>) => {
|
|
173
|
-
const columnData
|
|
176
|
+
const { columnData, tooltip } = renderData<T>(column, data);
|
|
174
177
|
|
|
175
178
|
return (
|
|
176
179
|
<div
|
|
@@ -179,14 +182,16 @@ export const DynamicListRow = <T extends Data>({
|
|
|
179
182
|
column.dataEntryRender !== undefined && allowEditing,
|
|
180
183
|
})}
|
|
181
184
|
key={column.key ?? (column.propertyName as string)}
|
|
185
|
+
style={{
|
|
186
|
+
justifyContent: horizontalTextAlign,
|
|
187
|
+
alignItems: verticalTextAlign,
|
|
188
|
+
}}
|
|
182
189
|
>
|
|
183
190
|
<div
|
|
184
|
-
className={classes.column
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: undefined
|
|
189
|
-
}
|
|
191
|
+
className={clsx(classes.column, {
|
|
192
|
+
[classes.nowrap]: !textWrap,
|
|
193
|
+
})}
|
|
194
|
+
title={tooltip}
|
|
190
195
|
data-test-id={`dynamic-list-property:${
|
|
191
196
|
column.propertyName as string
|
|
192
197
|
}`}
|
|
@@ -226,22 +231,26 @@ export const DynamicListRow = <T extends Data>({
|
|
|
226
231
|
</div>
|
|
227
232
|
);
|
|
228
233
|
};
|
|
229
|
-
|
|
230
|
-
const renderData = function <T extends Data>(
|
|
234
|
+
const renderData = <T extends Data>(
|
|
231
235
|
column: DynamicListColumn<T>,
|
|
232
236
|
data: T,
|
|
233
|
-
): React.ReactNode {
|
|
237
|
+
): { columnData: React.ReactNode; tooltip: string | undefined } => {
|
|
238
|
+
const getTooltip = (value: unknown): string | undefined =>
|
|
239
|
+
column.tooltip !== false ? getTooltipText(value) : undefined;
|
|
240
|
+
|
|
234
241
|
if (!column.propertyName) {
|
|
235
|
-
|
|
242
|
+
const columnData = column.render?.(undefined, data);
|
|
243
|
+
return { columnData, tooltip: getTooltip(columnData) };
|
|
236
244
|
}
|
|
237
245
|
const value: unknown = data[column.propertyName];
|
|
238
246
|
if (column.render) {
|
|
239
|
-
|
|
247
|
+
const columnData = column.render(value, data);
|
|
248
|
+
return { columnData, tooltip: getTooltip(columnData) };
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
if (value === null || value === undefined) {
|
|
243
|
-
return
|
|
252
|
+
return { columnData: <span />, tooltip: undefined };
|
|
244
253
|
}
|
|
245
254
|
|
|
246
|
-
return String(value);
|
|
255
|
+
return { columnData: String(value), tooltip: getTooltip(String(value)) };
|
|
247
256
|
};
|
|
@@ -92,6 +92,9 @@ export interface ExplorerProps<T extends Data> {
|
|
|
92
92
|
/** Vertical alignment of text */
|
|
93
93
|
verticalTextAlign?: 'start' | 'center' | 'end';
|
|
94
94
|
|
|
95
|
+
/** If set to true, column text overflow will be wrapped to a new line. Otherwise, it will be truncated with an ellipsis (default: true) */
|
|
96
|
+
textWrap?: boolean;
|
|
97
|
+
|
|
95
98
|
/** Defines when the loading of the next page is triggered. The number represents the number of row left, before a load is triggered. (default: 10) */
|
|
96
99
|
loadingTriggerOffset?: number;
|
|
97
100
|
|
|
@@ -183,6 +186,7 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
|
|
|
183
186
|
headerRowActionSize,
|
|
184
187
|
horizontalTextAlign,
|
|
185
188
|
verticalTextAlign,
|
|
189
|
+
textWrap,
|
|
186
190
|
|
|
187
191
|
columns,
|
|
188
192
|
filterOptions,
|
|
@@ -479,6 +483,7 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
|
|
|
479
483
|
headerRowActionSize={headerRowActionSize}
|
|
480
484
|
horizontalTextAlign={horizontalTextAlign}
|
|
481
485
|
verticalTextAlign={verticalTextAlign}
|
|
486
|
+
textWrap={textWrap}
|
|
482
487
|
keyProperty={keyProperty}
|
|
483
488
|
showActionButton={Boolean(generateItemLink) || Boolean(onItemClicked)} // or hard code to `true`?
|
|
484
489
|
selectionMode={mode}
|
|
@@ -21,6 +21,7 @@ import { SearcheableOptionsFilter } from '../SelectionTypes/SearcheableOptionsFi
|
|
|
21
21
|
import classes from './Filter.scss';
|
|
22
22
|
|
|
23
23
|
export interface FilterProps<T extends Data> {
|
|
24
|
+
selectOnFocus?: boolean;
|
|
24
25
|
options: FilterType<T>;
|
|
25
26
|
value?: FilterValue;
|
|
26
27
|
index?: number;
|
|
@@ -42,6 +43,7 @@ export const Filter = <T extends Data>({
|
|
|
42
43
|
onFilterChange,
|
|
43
44
|
onFilterClicked,
|
|
44
45
|
onValidate,
|
|
46
|
+
selectOnFocus = true,
|
|
45
47
|
}: PropsWithChildren<FilterProps<T>>): JSX.Element => {
|
|
46
48
|
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
|
47
49
|
const [hasError, setHasError] = useState<boolean>(false);
|
|
@@ -116,6 +118,7 @@ export const Filter = <T extends Data>({
|
|
|
116
118
|
}
|
|
117
119
|
onError={onError}
|
|
118
120
|
onValidate={onValidate}
|
|
121
|
+
selectOnFocus={selectOnFocus}
|
|
119
122
|
/>
|
|
120
123
|
);
|
|
121
124
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shallow } from 'enzyme';
|
|
1
|
+
import { mount, shallow } from 'enzyme';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { noop } from '../../../../helpers/utils';
|
|
4
4
|
import { FreeTextFilter } from './FreeTextFilter';
|
|
@@ -42,4 +42,19 @@ describe('FreeTextFilter', () => {
|
|
|
42
42
|
|
|
43
43
|
expect(error).toBeDefined();
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it('selects text on focus when selectOnFocus is true and there is a value', () => {
|
|
47
|
+
const mockValue = 'test value';
|
|
48
|
+
const spy = jest.fn();
|
|
49
|
+
|
|
50
|
+
const wrapper = mount(
|
|
51
|
+
<FreeTextFilter onSelect={spy} value={mockValue} selectOnFocus={true} />,
|
|
52
|
+
);
|
|
53
|
+
const input = wrapper.find('input');
|
|
54
|
+
input.simulate('focus');
|
|
55
|
+
|
|
56
|
+
const inputElement = input.getDOMNode<HTMLInputElement>();
|
|
57
|
+
expect(inputElement.selectionStart).toBe(0);
|
|
58
|
+
expect(inputElement.selectionEnd).toBe(mockValue.length);
|
|
59
|
+
});
|
|
45
60
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import React, { KeyboardEventHandler, useState } from 'react';
|
|
2
|
+
import React, { KeyboardEventHandler, useRef, useState } from 'react';
|
|
3
3
|
import { noop } from '../../../../helpers/utils';
|
|
4
4
|
import { FilterValidationResult, FilterValue } from '../../Filters.model';
|
|
5
5
|
import classes from './FreeTextFilter.scss';
|
|
@@ -17,6 +17,8 @@ export interface FreeTextFilterProps {
|
|
|
17
17
|
|
|
18
18
|
/** CSS Class name for additional styles */
|
|
19
19
|
className?: string;
|
|
20
|
+
/** Select text on focus if true (default: true) */
|
|
21
|
+
selectOnFocus?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export const FreeTextFilter: React.FC<FreeTextFilterProps> = ({
|
|
@@ -25,11 +27,19 @@ export const FreeTextFilter: React.FC<FreeTextFilterProps> = ({
|
|
|
25
27
|
onError = noop,
|
|
26
28
|
onValidate: customValidate,
|
|
27
29
|
className = '',
|
|
30
|
+
selectOnFocus = true,
|
|
28
31
|
}) => {
|
|
29
32
|
const [errorMsg, setErrorMsg] = useState<string>();
|
|
30
33
|
const ENTER_KEY = 'Enter';
|
|
31
34
|
|
|
32
35
|
const [valueLocal, setValue] = useState(value || '');
|
|
36
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
37
|
+
|
|
38
|
+
const onFocusWrapper = (): void => {
|
|
39
|
+
if (selectOnFocus && valueLocal) {
|
|
40
|
+
inputRef.current?.select();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
33
43
|
|
|
34
44
|
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (e) => {
|
|
35
45
|
if (e.key === ENTER_KEY) {
|
|
@@ -55,11 +65,13 @@ export const FreeTextFilter: React.FC<FreeTextFilterProps> = ({
|
|
|
55
65
|
)}
|
|
56
66
|
>
|
|
57
67
|
<input
|
|
68
|
+
ref={inputRef}
|
|
58
69
|
autoFocus
|
|
59
70
|
className={clsx(classes.inputValue, errorMsg && classes.hasError)}
|
|
60
71
|
onKeyDown={handleKeyDown}
|
|
61
72
|
value={valueLocal as string}
|
|
62
73
|
onChange={(e) => setValue(e.target.value)}
|
|
74
|
+
onFocus={onFocusWrapper}
|
|
63
75
|
/>
|
|
64
76
|
{errorMsg !== undefined && <small>{errorMsg}</small>}
|
|
65
77
|
</div>
|
|
@@ -54,7 +54,7 @@ const generateData = (amount: number): ListStoryData[] =>
|
|
|
54
54
|
generateItemArray(amount, (index) => ({
|
|
55
55
|
id: index + 1,
|
|
56
56
|
desc: `Description ${index + 1}: ${faker.lorem.words(
|
|
57
|
-
faker.datatype.number({ min: 10, max:
|
|
57
|
+
faker.datatype.number({ min: 10, max: 50 }),
|
|
58
58
|
)}`,
|
|
59
59
|
title: `Item ${index + 1}: ${faker.random.words(
|
|
60
60
|
faker.datatype.number({ min: 1, max: 3 }),
|
|
@@ -92,6 +92,7 @@ const groups = createGroups({
|
|
|
92
92
|
Styling: [
|
|
93
93
|
'horizontalTextAlign',
|
|
94
94
|
'verticalTextAlign',
|
|
95
|
+
'textWrap',
|
|
95
96
|
'minimumWidth',
|
|
96
97
|
'columnGap',
|
|
97
98
|
'rowGap',
|
|
@@ -65,6 +65,8 @@ export interface ListProps<T extends Data> {
|
|
|
65
65
|
horizontalTextAlign?: 'left' | 'right' | 'center';
|
|
66
66
|
/** Vertical alignment of text */
|
|
67
67
|
verticalTextAlign?: 'start' | 'center' | 'end';
|
|
68
|
+
/** If set to true, column text overflow will be wrapped to a new line. Otherwise, it will be truncated with an ellipsis (default: false) */
|
|
69
|
+
textWrap?: boolean;
|
|
68
70
|
/**
|
|
69
71
|
* Determines which select mode the list is in. (default: ListSelectMode.None)
|
|
70
72
|
* If 'Single' is selected, a check mark will be rendered for each row of data
|
|
@@ -122,7 +124,7 @@ const ListRenderer = <T extends Data>(
|
|
|
122
124
|
columnGap = '5px',
|
|
123
125
|
rowGap = '0px',
|
|
124
126
|
headerRowHeight = '44px',
|
|
125
|
-
listRowHeight
|
|
127
|
+
listRowHeight,
|
|
126
128
|
listRowActionSize = '50px',
|
|
127
129
|
headerRowActionSize = '28px',
|
|
128
130
|
horizontalTextAlign = 'left',
|
|
@@ -134,6 +136,7 @@ const ListRenderer = <T extends Data>(
|
|
|
134
136
|
selectionMode = ListSelectMode.None,
|
|
135
137
|
enableSelectAll = true,
|
|
136
138
|
enableSelectAllDeselect = false,
|
|
139
|
+
textWrap,
|
|
137
140
|
onItemClicked = noop,
|
|
138
141
|
onItemSelected = noop,
|
|
139
142
|
onRequestMoreData = noop,
|
|
@@ -300,6 +303,7 @@ const ListRenderer = <T extends Data>(
|
|
|
300
303
|
actionSize={listRowActionSize}
|
|
301
304
|
horizontalTextAlign={horizontalTextAlign}
|
|
302
305
|
verticalTextAlign={verticalTextAlign}
|
|
306
|
+
textWrap={textWrap}
|
|
303
307
|
selectionMode={selectionMode}
|
|
304
308
|
showActionButton={
|
|
305
309
|
selectionMode === ListSelectMode.None &&
|
|
@@ -29,17 +29,24 @@
|
|
|
29
29
|
|
|
30
30
|
.cellWrapper {
|
|
31
31
|
display: grid;
|
|
32
|
-
align-content: center;
|
|
33
32
|
width: 100%;
|
|
34
33
|
height: 100%;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
.cell {
|
|
38
|
-
white-space: nowrap;
|
|
39
|
-
text-overflow: ellipsis;
|
|
40
|
-
overflow: hidden;
|
|
41
37
|
max-width: 100%;
|
|
42
38
|
max-height: 100%;
|
|
39
|
+
|
|
40
|
+
span {
|
|
41
|
+
padding: 5px 0;
|
|
42
|
+
display: grid;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&.nowrap {
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
text-overflow: ellipsis;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
.actions {
|
|
@@ -566,4 +566,39 @@ describe('ListRow', () => {
|
|
|
566
566
|
expect(cell.prop('title')).toBe(mockValue);
|
|
567
567
|
});
|
|
568
568
|
});
|
|
569
|
+
|
|
570
|
+
describe('column text alignments', () => {
|
|
571
|
+
const alignments: {
|
|
572
|
+
horizontal: 'left' | 'right' | 'center';
|
|
573
|
+
vertical: 'center' | 'start' | 'end';
|
|
574
|
+
}[] = [
|
|
575
|
+
{ horizontal: 'center', vertical: 'center' },
|
|
576
|
+
{ horizontal: 'left', vertical: 'start' },
|
|
577
|
+
{ horizontal: 'right', vertical: 'end' },
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
alignments.forEach(({ horizontal, vertical }) => {
|
|
581
|
+
it(`should apply the correct styles for textAlign: ${horizontal}, alignSelf: ${vertical}`, () => {
|
|
582
|
+
const wrapper = mount(
|
|
583
|
+
<ListRow
|
|
584
|
+
{...mockProps}
|
|
585
|
+
columns={[{ propertyName: 'id', size: '1fr', label: 'id' }]}
|
|
586
|
+
horizontalTextAlign={horizontal}
|
|
587
|
+
verticalTextAlign={vertical}
|
|
588
|
+
/>,
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const wrapperDivs = wrapper.findWhere((node) =>
|
|
592
|
+
node.prop('data-test-id')?.startsWith('list-entry-property'),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
wrapperDivs.forEach((node) => {
|
|
596
|
+
const style = node.prop('style');
|
|
597
|
+
|
|
598
|
+
expect(style).toHaveProperty('textAlign', horizontal);
|
|
599
|
+
expect(style).toHaveProperty('alignSelf', vertical);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
});
|
|
569
604
|
});
|
|
@@ -19,13 +19,15 @@ export interface ListRowProps<T extends Data> {
|
|
|
19
19
|
/** Header row height */
|
|
20
20
|
columnGap: string;
|
|
21
21
|
/** List row height */
|
|
22
|
-
rowHeight
|
|
22
|
+
rowHeight?: string;
|
|
23
23
|
/** Size of action button and checkbox */
|
|
24
24
|
actionSize: string;
|
|
25
25
|
/** Horizontal alignment of text */
|
|
26
26
|
horizontalTextAlign: 'left' | 'right' | 'center';
|
|
27
27
|
/** Vertical alignment of text */
|
|
28
28
|
verticalTextAlign: 'start' | 'center' | 'end';
|
|
29
|
+
/** If set to true, column text overflow will be wrapped to a new line. Otherwise, it will be truncated with an ellipsis (default: false) */
|
|
30
|
+
textWrap?: boolean;
|
|
29
31
|
/** List data */
|
|
30
32
|
data: T;
|
|
31
33
|
/** The column definition */
|
|
@@ -81,32 +83,54 @@ export const setLocale = (locale: string): void => {
|
|
|
81
83
|
|
|
82
84
|
setLocale(navigator.language);
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
const renderData = <T extends Data>(
|
|
85
87
|
column: Column<T>,
|
|
86
88
|
rowData: T,
|
|
87
|
-
): React.ReactNode {
|
|
89
|
+
): { columnData: React.ReactNode; tooltip: string | undefined } => {
|
|
90
|
+
const getTooltip = (value: unknown): string | undefined =>
|
|
91
|
+
column.tooltip !== false ? getTooltipText(value) : undefined;
|
|
92
|
+
|
|
88
93
|
if (!column.propertyName) {
|
|
89
|
-
|
|
94
|
+
const columnData = column.render?.(undefined, rowData);
|
|
95
|
+
return {
|
|
96
|
+
columnData,
|
|
97
|
+
tooltip: getTooltip(columnData),
|
|
98
|
+
};
|
|
90
99
|
}
|
|
91
100
|
const value: unknown = rowData[column.propertyName];
|
|
92
101
|
if (column.render) {
|
|
93
|
-
|
|
102
|
+
const columnData = column.render(value, rowData);
|
|
103
|
+
return {
|
|
104
|
+
columnData,
|
|
105
|
+
tooltip: getTooltip(columnData),
|
|
106
|
+
};
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
if (value === null || value === undefined) {
|
|
97
|
-
return
|
|
110
|
+
return { columnData: <span />, tooltip: undefined };
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
if (typeof value === 'number') {
|
|
101
|
-
|
|
114
|
+
const columnData = numberFormatter.format(value);
|
|
115
|
+
return {
|
|
116
|
+
columnData: <span>{columnData}</span>,
|
|
117
|
+
tooltip: getTooltip(columnData),
|
|
118
|
+
};
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
if (value instanceof Date) {
|
|
105
|
-
|
|
122
|
+
const columnData = dateFormatter.format(value);
|
|
123
|
+
return {
|
|
124
|
+
columnData: <span>{columnData}</span>,
|
|
125
|
+
tooltip: getTooltip(columnData),
|
|
126
|
+
};
|
|
106
127
|
}
|
|
107
128
|
|
|
108
|
-
return
|
|
109
|
-
}
|
|
129
|
+
return {
|
|
130
|
+
columnData: <span>{String(value)}</span>,
|
|
131
|
+
tooltip: getTooltip(String(value)),
|
|
132
|
+
};
|
|
133
|
+
};
|
|
110
134
|
|
|
111
135
|
/**
|
|
112
136
|
* Renders the rows for the list component
|
|
@@ -128,6 +152,7 @@ export const ListRow = <T extends Data>({
|
|
|
128
152
|
actionSize,
|
|
129
153
|
horizontalTextAlign,
|
|
130
154
|
verticalTextAlign,
|
|
155
|
+
textWrap = false,
|
|
131
156
|
data,
|
|
132
157
|
itemSelected = false,
|
|
133
158
|
isTrigger = false,
|
|
@@ -143,7 +168,7 @@ export const ListRow = <T extends Data>({
|
|
|
143
168
|
inlineMenuActions,
|
|
144
169
|
}: PropsWithChildren<ListRowProps<T>>): JSX.Element => {
|
|
145
170
|
const customRootStyles = {
|
|
146
|
-
gridAutoRows: rowHeight
|
|
171
|
+
gridAutoRows: `minmax(50px, ${rowHeight})`,
|
|
147
172
|
gridColumnGap: columnGap,
|
|
148
173
|
justifyItems: horizontalTextAlign,
|
|
149
174
|
alignItems: verticalTextAlign,
|
|
@@ -199,7 +224,7 @@ export const ListRow = <T extends Data>({
|
|
|
199
224
|
typeof onItemClicked !== 'function';
|
|
200
225
|
|
|
201
226
|
const generateCells: JSX.Element[] = columns.map((column: Column<T>) => {
|
|
202
|
-
const columnData
|
|
227
|
+
const { columnData, tooltip } = renderData<T>(column, data);
|
|
203
228
|
|
|
204
229
|
return (
|
|
205
230
|
<div
|
|
@@ -207,12 +232,14 @@ export const ListRow = <T extends Data>({
|
|
|
207
232
|
className={classes.cellWrapper}
|
|
208
233
|
>
|
|
209
234
|
<div
|
|
210
|
-
className={classes.cell}
|
|
211
|
-
title={
|
|
212
|
-
column.tooltip !== false ? getTooltipText(columnData) : undefined
|
|
213
|
-
}
|
|
235
|
+
className={clsx(classes.cell, { [classes.nowrap]: !textWrap })}
|
|
236
|
+
title={tooltip}
|
|
214
237
|
data-test-id={`list-entry-property:${column.propertyName as string}`}
|
|
215
|
-
style={{
|
|
238
|
+
style={{
|
|
239
|
+
justifySelf: column.horizontalColumnAlign, // Horizontal alignment based on column config
|
|
240
|
+
alignSelf: verticalTextAlign, // Vertical alignment based on props
|
|
241
|
+
textAlign: horizontalTextAlign, // Additional text alignment inside the cell
|
|
242
|
+
}}
|
|
216
243
|
>
|
|
217
244
|
{columnData}
|
|
218
245
|
</div>
|
|
@@ -10,7 +10,7 @@ export interface ListRowLoaderProps<T extends Data> {
|
|
|
10
10
|
/** Space between columns */
|
|
11
11
|
columnGap: string;
|
|
12
12
|
/** List row height */
|
|
13
|
-
rowHeight
|
|
13
|
+
rowHeight?: string;
|
|
14
14
|
/** The column definition */
|
|
15
15
|
columns: Column<T>[];
|
|
16
16
|
}
|
|
@@ -25,7 +25,7 @@ export const ListRowLoader = <T extends Data>({
|
|
|
25
25
|
const rows = 5;
|
|
26
26
|
|
|
27
27
|
const customRowStyles = {
|
|
28
|
-
gridAutoRows: rowHeight
|
|
28
|
+
gridAutoRows: `minmax(50px, ${rowHeight})`,
|
|
29
29
|
gridTemplateColumns: columnSizes,
|
|
30
30
|
gridColumnGap: columnGap,
|
|
31
31
|
} as React.CSSProperties;
|