@axinom/mosaic-ui 0.66.0-rc.11 → 0.66.0-rc.12
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/DynamicListDataEntry/DynamicListDataEntry.d.ts.map +1 -1
- package/dist/components/DynamicDataList/DynamicListRow/DynamicListRow.d.ts.map +1 -1
- package/dist/components/Filters/Filter/Filter.d.ts.map +1 -1
- package/dist/components/Filters/Filters.model.d.ts +5 -0
- package/dist/components/Filters/Filters.model.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.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/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.d.ts.map +1 -1
- package/dist/components/FormElements/Radio/Radio.d.ts.map +1 -1
- package/dist/components/FormElements/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/components/Hub/Tile/Tile.d.ts.map +1 -1
- package/dist/components/Icons/Icons.d.ts +4 -9
- package/dist/components/Icons/Icons.d.ts.map +1 -1
- package/dist/components/LandingPageTiles/TileLarge/TileLarge.d.ts.map +1 -1
- package/dist/components/LandingPageTiles/TileSmall/TileSmall.d.ts.map +1 -1
- package/dist/components/List/ListHeader/ColumnLabel/ColumnLabel.d.ts.map +1 -1
- package/dist/components/Loaders/ImageLoader/ImageLoader.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
- package/dist/components/VisualElements/ImgElement.d.ts +50 -0
- package/dist/components/VisualElements/ImgElement.d.ts.map +1 -0
- package/dist/components/VisualElements/SvgElement.d.ts +14 -0
- package/dist/components/VisualElements/SvgElement.d.ts.map +1 -0
- package/dist/components/VisualElements/index.d.ts +3 -0
- package/dist/components/VisualElements/index.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.es.js +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/DynamicDataList/DynamicListDataEntry/DynamicListDataEntry.spec.tsx +3 -3
- package/src/components/DynamicDataList/DynamicListDataEntry/DynamicListDataEntry.tsx +4 -1
- package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.tsx +4 -1
- package/src/components/Filters/Filter/Filter.scss +1 -1
- package/src/components/Filters/Filter/Filter.tsx +27 -1
- package/src/components/Filters/Filters.model.ts +6 -0
- package/src/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.tsx +5 -0
- package/src/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.tsx +4 -0
- package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.tsx +9 -1
- package/src/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.tsx +5 -0
- package/src/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.tsx +8 -0
- package/src/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.tsx +6 -1
- package/src/components/FormElements/Radio/Radio.tsx +3 -2
- package/src/components/FormElements/ToggleButton/ToggleButton.tsx +32 -27
- package/src/components/Hub/Hub.stories.tsx +3 -2
- package/src/components/Hub/Tile/Tile.spec.tsx +7 -2
- package/src/components/Hub/Tile/Tile.tsx +2 -1
- package/src/components/Icons/Icons.scss +1 -0
- package/src/components/Icons/Icons.spec.tsx +90 -41
- package/src/components/Icons/Icons.tsx +357 -765
- package/src/components/LandingPageTiles/LandingPageTiles.stories.tsx +3 -2
- package/src/components/LandingPageTiles/TileLarge/TileLarge.spec.tsx +5 -1
- package/src/components/LandingPageTiles/TileLarge/TileLarge.tsx +2 -1
- package/src/components/LandingPageTiles/TileSmall/TileSmall.spec.tsx +7 -2
- package/src/components/LandingPageTiles/TileSmall/TileSmall.tsx +2 -1
- package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.spec.tsx +2 -2
- package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.tsx +5 -12
- package/src/components/Loaders/ImageLoader/ImageLoader.spec.tsx +13 -14
- package/src/components/Loaders/ImageLoader/ImageLoader.tsx +5 -3
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +13 -2
- package/src/components/VisualElements/ImgElement.spec.tsx +92 -0
- package/src/components/VisualElements/ImgElement.tsx +72 -0
- package/src/components/VisualElements/SvgElement.spec.tsx +160 -0
- package/src/components/VisualElements/SvgElement.tsx +40 -0
- package/src/components/VisualElements/index.ts +7 -0
- package/src/components/index.ts +1 -0
|
@@ -2,6 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { MemoryRouter } from 'react-router-dom';
|
|
4
4
|
import { createGroups, getRandomInt } from '../../helpers/storybook';
|
|
5
|
+
import { SvgElement } from '../VisualElements/SvgElement';
|
|
5
6
|
import { LandingPageTiles } from './LandingPageTiles';
|
|
6
7
|
import { LandingPageItem } from './LandingPageTiles.model';
|
|
7
8
|
|
|
@@ -15,7 +16,7 @@ const defaultContainer = {
|
|
|
15
16
|
|
|
16
17
|
const DefaultIcon: React.FC = () => {
|
|
17
18
|
return (
|
|
18
|
-
<
|
|
19
|
+
<SvgElement viewBox="0 0 40 40">
|
|
19
20
|
<path
|
|
20
21
|
vectorEffect="non-scaling-stroke"
|
|
21
22
|
fill="none"
|
|
@@ -23,7 +24,7 @@ const DefaultIcon: React.FC = () => {
|
|
|
23
24
|
d="M39,39H1V1h38V39z M39.1,26l-8.4-8.7l-9.1,11.5
|
|
24
25
|
l-6.4-5.4L3.6,39.1 M12.8,7.8c-2.4,0-4.4,2-4.4,4.4s2,4.4,4.4,4.4s4.4-2,4.4-4.4S15.3,7.8,12.8,7.8z"
|
|
25
26
|
/>
|
|
26
|
-
</
|
|
27
|
+
</SvgElement>
|
|
27
28
|
);
|
|
28
29
|
};
|
|
29
30
|
|
|
@@ -84,7 +84,11 @@ describe('TileLarge', () => {
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
it('renders an icon when passed as a string URL', () => {
|
|
87
|
-
const wrapper =
|
|
87
|
+
const wrapper = mount(
|
|
88
|
+
<MemoryRouter>
|
|
89
|
+
<TileLarge {...mockProps} />
|
|
90
|
+
</MemoryRouter>,
|
|
91
|
+
);
|
|
88
92
|
const iconUrl = wrapper.find('img').prop('src');
|
|
89
93
|
|
|
90
94
|
expect(iconUrl).toBe(`${mockProps.icon}`);
|
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { Link } from 'react-router-dom';
|
|
4
4
|
import { useValueOrOnDemand } from '../../../hooks/useValueOrOnDemand/useValueOrOnDemand';
|
|
5
5
|
import { Loader } from '../../Loaders/Loader/Loader';
|
|
6
|
+
import { ImgElement } from '../../VisualElements';
|
|
6
7
|
import { LandingPageItem } from '../LandingPageTiles.model';
|
|
7
8
|
import classes from './TileLarge.scss';
|
|
8
9
|
|
|
@@ -56,7 +57,7 @@ export const TileLarge: React.FC<TileLargeProps> = ({
|
|
|
56
57
|
{React.isValidElement(icon)
|
|
57
58
|
? icon
|
|
58
59
|
: typeof icon === 'string' && (
|
|
59
|
-
<
|
|
60
|
+
<ImgElement src={icon} decorative={true} />
|
|
60
61
|
)}
|
|
61
62
|
</div>
|
|
62
63
|
<div className={classes.titlesSection}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { shallow } from 'enzyme';
|
|
1
|
+
import { mount, shallow } from 'enzyme';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { MemoryRouter } from 'react-router';
|
|
3
4
|
import { TileSmall, TileSmallProps } from './TileSmall';
|
|
4
5
|
|
|
5
6
|
const mockProps: TileSmallProps = {
|
|
@@ -31,7 +32,11 @@ describe('TileSmall', () => {
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
it('renders an icon when passed a string URL', () => {
|
|
34
|
-
const wrapper =
|
|
35
|
+
const wrapper = mount(
|
|
36
|
+
<MemoryRouter>
|
|
37
|
+
<TileSmall {...mockProps} />
|
|
38
|
+
</MemoryRouter>,
|
|
39
|
+
);
|
|
35
40
|
const iconUrl = wrapper.find('img').prop('src');
|
|
36
41
|
|
|
37
42
|
expect(iconUrl).toBe(`${mockProps.icon}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Link } from 'react-router-dom';
|
|
4
|
+
import { ImgElement } from '../../VisualElements';
|
|
4
5
|
import { LandingPageItem } from '../LandingPageTiles.model';
|
|
5
6
|
import classes from './TileSmall.scss';
|
|
6
7
|
|
|
@@ -50,7 +51,7 @@ export const TileSmall: React.FC<TileSmallProps> = ({
|
|
|
50
51
|
{React.isValidElement(icon)
|
|
51
52
|
? icon
|
|
52
53
|
: typeof icon === 'string' && (
|
|
53
|
-
<
|
|
54
|
+
<ImgElement src={icon} decorative={true} />
|
|
54
55
|
)}
|
|
55
56
|
</div>
|
|
56
57
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shallow } from 'enzyme';
|
|
1
|
+
import { mount, shallow } from 'enzyme';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { SortData } from '../../List.model';
|
|
4
4
|
import { ColumnLabel } from './ColumnLabel';
|
|
@@ -37,7 +37,7 @@ describe('ColumnLabel', () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('renders the directional arrows by default', () => {
|
|
40
|
-
const wrapper =
|
|
40
|
+
const wrapper = mount(<ColumnLabel propertyName="test-prop" />);
|
|
41
41
|
|
|
42
42
|
const dirArrows = wrapper.find('svg');
|
|
43
43
|
const clickWrapper = wrapper.find('.clickWrapper');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import React, { PropsWithChildren } from 'react';
|
|
3
3
|
import { noop } from '../../../../helpers/utils';
|
|
4
|
+
import { SvgElement } from '../../../VisualElements/SvgElement';
|
|
4
5
|
import { ColumnSortKeys, SortData } from '../../List.model';
|
|
5
6
|
import classes from './ColumnLabel.scss';
|
|
6
7
|
|
|
@@ -73,26 +74,18 @@ export const ColumnLabel = <T,>({
|
|
|
73
74
|
{label ?? propertyName}
|
|
74
75
|
</span>
|
|
75
76
|
<div className={classes.arrows}>
|
|
76
|
-
<
|
|
77
|
-
version="1.1"
|
|
78
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
79
|
-
viewBox="0 0 13 10"
|
|
80
|
-
>
|
|
77
|
+
<SvgElement viewBox="0 0 13 10">
|
|
81
78
|
<path
|
|
82
79
|
className={clsx({ [classes.sorted]: direction === 'asc' })}
|
|
83
80
|
d="M1.5,7.3l4.8-5.7l5.2,5.7"
|
|
84
81
|
></path>
|
|
85
|
-
</
|
|
86
|
-
<
|
|
87
|
-
version="1.1"
|
|
88
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
89
|
-
viewBox="0 0 13 10"
|
|
90
|
-
>
|
|
82
|
+
</SvgElement>
|
|
83
|
+
<SvgElement viewBox="0 0 13 10">
|
|
91
84
|
<path
|
|
92
85
|
className={clsx({ [classes.sorted]: direction === 'desc' })}
|
|
93
86
|
d="M11.5,2.8L6.7,8.5L1.5,2.8"
|
|
94
87
|
></path>
|
|
95
|
-
</
|
|
88
|
+
</SvgElement>
|
|
96
89
|
</div>
|
|
97
90
|
</div>
|
|
98
91
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
-
import { shallow } from 'enzyme';
|
|
2
|
+
import { mount, shallow } from 'enzyme';
|
|
3
3
|
import React, { ReactNode, SyntheticEvent } from 'react';
|
|
4
4
|
import ContentLoader from 'react-content-loader';
|
|
5
5
|
import { act } from 'react-dom/test-utils';
|
|
@@ -18,7 +18,7 @@ describe('ImageLoader', () => {
|
|
|
18
18
|
const mockWidth = 'test-width';
|
|
19
19
|
const mockAlt = 'test-alt';
|
|
20
20
|
|
|
21
|
-
const wrapper =
|
|
21
|
+
const wrapper = mount(
|
|
22
22
|
<ImageLoader
|
|
23
23
|
imgSrc={mockSrc}
|
|
24
24
|
alt={mockAlt}
|
|
@@ -39,7 +39,7 @@ describe('ImageLoader', () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('renders the loading animation while an image is loading', () => {
|
|
42
|
-
const wrapper =
|
|
42
|
+
const wrapper = mount(<ImageLoader imgSrc="" />);
|
|
43
43
|
|
|
44
44
|
const loader = wrapper.find(ContentLoader);
|
|
45
45
|
const imgDisplay = wrapper.find('img').prop('style')?.display;
|
|
@@ -49,9 +49,10 @@ describe('ImageLoader', () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it('renders the image after loading has completed', () => {
|
|
52
|
-
const wrapper =
|
|
52
|
+
const wrapper = mount(<ImageLoader imgSrc="" />);
|
|
53
53
|
|
|
54
54
|
wrapper.find('img').prop('onLoad')!({} as SyntheticEvent);
|
|
55
|
+
wrapper.update();
|
|
55
56
|
|
|
56
57
|
const loader = wrapper.find(ContentLoader);
|
|
57
58
|
const imgDisplay = wrapper.find('img').prop('style')?.display;
|
|
@@ -63,7 +64,7 @@ describe('ImageLoader', () => {
|
|
|
63
64
|
it('emits onLoad callback with img src after successful load', () => {
|
|
64
65
|
const spy = jest.fn();
|
|
65
66
|
const mockUrl = 'mock-url';
|
|
66
|
-
const wrapper =
|
|
67
|
+
const wrapper = mount(<ImageLoader imgSrc={mockUrl} onLoad={spy} />);
|
|
67
68
|
|
|
68
69
|
wrapper.find('img').prop('onLoad')!({} as SyntheticEvent);
|
|
69
70
|
|
|
@@ -77,9 +78,7 @@ describe('ImageLoader', () => {
|
|
|
77
78
|
};
|
|
78
79
|
const spy = jest.fn();
|
|
79
80
|
const mockUrl = 'mock-url';
|
|
80
|
-
const wrapper =
|
|
81
|
-
<ImageLoader imgSrc={mockUrl} onImageClick={spy} />,
|
|
82
|
-
);
|
|
81
|
+
const wrapper = mount(<ImageLoader imgSrc={mockUrl} onImageClick={spy} />);
|
|
83
82
|
|
|
84
83
|
const image = wrapper.find('img');
|
|
85
84
|
|
|
@@ -93,9 +92,10 @@ describe('ImageLoader', () => {
|
|
|
93
92
|
});
|
|
94
93
|
|
|
95
94
|
it('renders the fallback background color when loading has failed', () => {
|
|
96
|
-
const wrapper =
|
|
95
|
+
const wrapper = mount(<ImageLoader imgSrc="" />);
|
|
97
96
|
|
|
98
97
|
wrapper.find('img').prop('onError')!({} as SyntheticEvent);
|
|
98
|
+
wrapper.update();
|
|
99
99
|
|
|
100
100
|
const loader = wrapper.find(ContentLoader);
|
|
101
101
|
const fallbackContainer = wrapper.find('.container').at(1);
|
|
@@ -107,11 +107,12 @@ describe('ImageLoader', () => {
|
|
|
107
107
|
|
|
108
108
|
it('renders a fallback image when loading has failed', () => {
|
|
109
109
|
const mockFallbackUrl = 'mock-url';
|
|
110
|
-
const wrapper =
|
|
110
|
+
const wrapper = mount(
|
|
111
111
|
<ImageLoader imgSrc="" fallbackSrc={mockFallbackUrl} />,
|
|
112
112
|
);
|
|
113
113
|
|
|
114
114
|
wrapper.find('img').prop('onError')!({} as SyntheticEvent);
|
|
115
|
+
wrapper.update();
|
|
115
116
|
|
|
116
117
|
const loader = wrapper.find(ContentLoader);
|
|
117
118
|
const images = wrapper.find('img');
|
|
@@ -127,7 +128,7 @@ describe('ImageLoader', () => {
|
|
|
127
128
|
it('emits onError callback with img src after load failure', () => {
|
|
128
129
|
const spy = jest.fn();
|
|
129
130
|
const mockUrl = 'mock-url';
|
|
130
|
-
const wrapper =
|
|
131
|
+
const wrapper = mount(<ImageLoader imgSrc={mockUrl} onError={spy} />);
|
|
131
132
|
|
|
132
133
|
wrapper.find('img').prop('onError')!({} as SyntheticEvent);
|
|
133
134
|
|
|
@@ -142,9 +143,7 @@ describe('ImageLoader', () => {
|
|
|
142
143
|
<circle r="50" />
|
|
143
144
|
</svg>
|
|
144
145
|
);
|
|
145
|
-
const wrapper =
|
|
146
|
-
<ImageLoader imgSrc="" loadingSkeleton={mockSVG} />,
|
|
147
|
-
);
|
|
146
|
+
const wrapper = mount(<ImageLoader imgSrc="" loadingSkeleton={mockSVG} />);
|
|
148
147
|
|
|
149
148
|
const svg = wrapper.find(`#${mockId}`);
|
|
150
149
|
|
|
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|
|
2
2
|
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
|
|
3
3
|
import ContentLoader from 'react-content-loader';
|
|
4
4
|
import { noop } from '../../../helpers/utils';
|
|
5
|
+
import { ImgElement } from '../../VisualElements/ImgElement';
|
|
5
6
|
import { SquareOutlineSkeleton } from '../skeletons';
|
|
6
7
|
import classes from './ImageLoader.scss';
|
|
7
8
|
|
|
@@ -140,7 +141,7 @@ export const ImageLoader: React.FC<ImageLoaderProps> = ({
|
|
|
140
141
|
)}
|
|
141
142
|
<div className={classes.imageContainer}>
|
|
142
143
|
{state !== ImageLoaderState.Failed && imgSrc !== undefined && (
|
|
143
|
-
<
|
|
144
|
+
<ImgElement
|
|
144
145
|
src={imgSrc}
|
|
145
146
|
height={imgHeight}
|
|
146
147
|
width={imgWidth}
|
|
@@ -149,7 +150,7 @@ export const ImageLoader: React.FC<ImageLoaderProps> = ({
|
|
|
149
150
|
objectFit: 'contain',
|
|
150
151
|
maxWidth: '100%',
|
|
151
152
|
}}
|
|
152
|
-
alt={alt}
|
|
153
|
+
alt={alt ? alt : 'Loaded content image'}
|
|
153
154
|
onLoad={onLoadHandler}
|
|
154
155
|
onError={onErrorHandler}
|
|
155
156
|
onClick={onImageClick}
|
|
@@ -159,11 +160,12 @@ export const ImageLoader: React.FC<ImageLoaderProps> = ({
|
|
|
159
160
|
)}
|
|
160
161
|
{state === ImageLoaderState.Failed &&
|
|
161
162
|
(fallbackSrc ? (
|
|
162
|
-
<
|
|
163
|
+
<ImgElement
|
|
163
164
|
className={classes.fallBackImage}
|
|
164
165
|
src={String(fallbackSrc)}
|
|
165
166
|
height={imgHeight}
|
|
166
167
|
width={imgWidth}
|
|
168
|
+
alt="Image unavailable"
|
|
167
169
|
/>
|
|
168
170
|
) : (
|
|
169
171
|
<div
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useConfirmationDelay,
|
|
10
10
|
} from '../../ConfirmDialog';
|
|
11
11
|
import { IconName, Icons } from '../../Icons';
|
|
12
|
+
import { ImgElement } from '../../VisualElements';
|
|
12
13
|
import {
|
|
13
14
|
PageHeaderActionProps,
|
|
14
15
|
PageHeaderActionType,
|
|
@@ -142,7 +143,12 @@ const PageHeaderJSAction: React.FC<PageHeaderJsActionProps> = ({
|
|
|
142
143
|
<div className={classes.icon}>
|
|
143
144
|
{!confirmation &&
|
|
144
145
|
(typeof icon === 'string' ? (
|
|
145
|
-
<
|
|
146
|
+
<ImgElement
|
|
147
|
+
src={icon}
|
|
148
|
+
{...(imgAlt
|
|
149
|
+
? { alt: imgAlt ?? `${label} icon` }
|
|
150
|
+
: { decorative: true })}
|
|
151
|
+
/>
|
|
146
152
|
) : (
|
|
147
153
|
<Icons icon={icon} className={classes.pageHeaderActionsIcons} />
|
|
148
154
|
))}
|
|
@@ -208,7 +214,12 @@ const PageHeaderNavigationAction: React.FC<PageHeaderNavigationActionProps> = ({
|
|
|
208
214
|
>
|
|
209
215
|
<div className={classes.icon}>
|
|
210
216
|
{typeof headerIcon === 'string' ? (
|
|
211
|
-
<
|
|
217
|
+
<ImgElement
|
|
218
|
+
src={headerIcon}
|
|
219
|
+
{...(imgAlt
|
|
220
|
+
? { alt: imgAlt ?? `${label} icon` }
|
|
221
|
+
: { decorative: true })}
|
|
222
|
+
/>
|
|
212
223
|
) : (
|
|
213
224
|
<Icons icon={headerIcon} className={classes.pageHeaderActionsIcons} />
|
|
214
225
|
)}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { shallow } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ImgElement } from './ImgElement';
|
|
4
|
+
|
|
5
|
+
describe('ImgElement', () => {
|
|
6
|
+
it('renders meaningful image with alt text', () => {
|
|
7
|
+
const alt = 'Test image';
|
|
8
|
+
const src = '/test.jpg';
|
|
9
|
+
const wrapper = shallow(<ImgElement src={src} alt={alt} />);
|
|
10
|
+
|
|
11
|
+
const img = wrapper.find('img');
|
|
12
|
+
expect(img.prop('src')).toBe(src);
|
|
13
|
+
expect(img.prop('alt')).toBe(alt);
|
|
14
|
+
expect(img.prop('aria-hidden')).toBeUndefined();
|
|
15
|
+
expect(img.prop('role')).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders decorative image with accessibility attributes', () => {
|
|
19
|
+
const src = '/decorative.jpg';
|
|
20
|
+
const wrapper = shallow(<ImgElement src={src} decorative={true} />);
|
|
21
|
+
|
|
22
|
+
const img = wrapper.find('img');
|
|
23
|
+
expect(img.prop('src')).toBe(src);
|
|
24
|
+
expect(img.prop('alt')).toBe('');
|
|
25
|
+
expect(img.prop('aria-hidden')).toBe('true');
|
|
26
|
+
expect(img.prop('role')).toBe('presentation');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('forwards additional HTML img attributes for meaningful images', () => {
|
|
30
|
+
const width = 100;
|
|
31
|
+
const height = 200;
|
|
32
|
+
const className = 'test-class';
|
|
33
|
+
|
|
34
|
+
const wrapper = shallow(
|
|
35
|
+
<ImgElement
|
|
36
|
+
src="/test.jpg"
|
|
37
|
+
alt="Test image"
|
|
38
|
+
width={width}
|
|
39
|
+
height={height}
|
|
40
|
+
className={className}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const img = wrapper.find('img');
|
|
45
|
+
expect(img.prop('width')).toBe(width);
|
|
46
|
+
expect(img.prop('height')).toBe(height);
|
|
47
|
+
expect(img.prop('className')).toBe(className);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('forwards additional HTML img attributes for decorative images', () => {
|
|
51
|
+
const width = 50;
|
|
52
|
+
const height = 60;
|
|
53
|
+
const className = 'icon';
|
|
54
|
+
|
|
55
|
+
const wrapper = shallow(
|
|
56
|
+
<ImgElement
|
|
57
|
+
src="/decorative.jpg"
|
|
58
|
+
decorative={true}
|
|
59
|
+
width={width}
|
|
60
|
+
height={height}
|
|
61
|
+
className={className}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const img = wrapper.find('img');
|
|
66
|
+
expect(img.prop('width')).toBe(width);
|
|
67
|
+
expect(img.prop('height')).toBe(height);
|
|
68
|
+
expect(img.prop('className')).toBe(className);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses meaningful image behavior by default', () => {
|
|
72
|
+
const alt = 'Default behavior';
|
|
73
|
+
const wrapper = shallow(<ImgElement src="/default.jpg" alt={alt} />);
|
|
74
|
+
|
|
75
|
+
const img = wrapper.find('img');
|
|
76
|
+
expect(img.prop('alt')).toBe(alt);
|
|
77
|
+
expect(img.prop('aria-hidden')).toBeUndefined();
|
|
78
|
+
expect(img.prop('role')).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders with decorative=false explicitly', () => {
|
|
82
|
+
const alt = 'Explicit meaningful';
|
|
83
|
+
const wrapper = shallow(
|
|
84
|
+
<ImgElement src="/explicit.jpg" alt={alt} decorative={false} />,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const img = wrapper.find('img');
|
|
88
|
+
expect(img.prop('alt')).toBe(alt);
|
|
89
|
+
expect(img.prop('aria-hidden')).toBeUndefined();
|
|
90
|
+
expect(img.prop('role')).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type BaseImgElementProps = Omit<
|
|
4
|
+
React.ImgHTMLAttributes<HTMLImageElement>,
|
|
5
|
+
'alt'
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
/** Props for decorative images - alt text not needed or allowed */
|
|
9
|
+
export interface DecorativeImageProps {
|
|
10
|
+
/** Whether the image is decorative (sets aria-hidden="true", alt="", and role="presentation") */
|
|
11
|
+
decorative: true;
|
|
12
|
+
/** Alt text - not needed when decorative */
|
|
13
|
+
alt?: never;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Props for meaningful images - alt text is required */
|
|
17
|
+
export interface MeaningfulImageProps {
|
|
18
|
+
/** Whether the image is decorative (sets aria-hidden="true", alt="", and role="presentation") */
|
|
19
|
+
decorative?: false;
|
|
20
|
+
/** Alt text - required for non-decorative images */
|
|
21
|
+
alt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Union type for all image accessibility configurations */
|
|
25
|
+
export type ImageAccessibilityProps =
|
|
26
|
+
| DecorativeImageProps
|
|
27
|
+
| MeaningfulImageProps;
|
|
28
|
+
|
|
29
|
+
export type ImgElementProps = BaseImgElementProps & ImageAccessibilityProps;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Centralized image component with proper accessibility handling.
|
|
33
|
+
* Provides consistent image rendering across the application with automatic
|
|
34
|
+
* accessibility attributes based on the decorative flag.
|
|
35
|
+
*
|
|
36
|
+
* For non-decorative images, alt text is required to ensure accessibility.
|
|
37
|
+
* For decorative images, alt text is automatically set to empty string.
|
|
38
|
+
*
|
|
39
|
+
* @example Basic image with required alt text
|
|
40
|
+
* <ImgElement
|
|
41
|
+
* src="/path/to/image.jpg"
|
|
42
|
+
* alt="Description of the image"
|
|
43
|
+
* />
|
|
44
|
+
*
|
|
45
|
+
* @example Decorative image (alt not required or allowed)
|
|
46
|
+
* <ImgElement
|
|
47
|
+
* src="/path/to/decorative.jpg"
|
|
48
|
+
* decorative={true}
|
|
49
|
+
* />
|
|
50
|
+
*
|
|
51
|
+
* @example Navigation icon
|
|
52
|
+
* <ImgElement
|
|
53
|
+
* src="/icons/menu.svg"
|
|
54
|
+
* decorative={true}
|
|
55
|
+
* width={24}
|
|
56
|
+
* height={24}
|
|
57
|
+
* />
|
|
58
|
+
*/
|
|
59
|
+
export const ImgElement: React.FC<ImgElementProps> = ({
|
|
60
|
+
decorative = false,
|
|
61
|
+
...imgProps
|
|
62
|
+
}) => {
|
|
63
|
+
const accessibilityProps = decorative
|
|
64
|
+
? {
|
|
65
|
+
'aria-hidden': 'true' as const,
|
|
66
|
+
alt: '',
|
|
67
|
+
role: 'presentation' as const,
|
|
68
|
+
}
|
|
69
|
+
: {};
|
|
70
|
+
|
|
71
|
+
return <img {...accessibilityProps} {...imgProps} />;
|
|
72
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { mount, shallow } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { SvgElement, SvgElementProps } from './SvgElement';
|
|
4
|
+
|
|
5
|
+
describe('SvgElement', () => {
|
|
6
|
+
it('renders the component without crashing', () => {
|
|
7
|
+
const wrapper = shallow(<SvgElement />);
|
|
8
|
+
|
|
9
|
+
expect(wrapper).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders with default viewBox', () => {
|
|
13
|
+
const wrapper = mount(<SvgElement />);
|
|
14
|
+
const svg = wrapper.find('svg');
|
|
15
|
+
|
|
16
|
+
expect(svg.prop('viewBox')).toBe('0 0 40 40');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renders with custom viewBox', () => {
|
|
20
|
+
const customViewBox = '0 0 24 24';
|
|
21
|
+
const wrapper = mount(<SvgElement viewBox={customViewBox} />);
|
|
22
|
+
const svg = wrapper.find('svg');
|
|
23
|
+
|
|
24
|
+
expect(svg.prop('viewBox')).toBe(customViewBox);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('passes down className to svg element', () => {
|
|
28
|
+
const testClass = 'custom-icon-class';
|
|
29
|
+
const wrapper = mount(<SvgElement className={testClass} />);
|
|
30
|
+
const svg = wrapper.find('svg');
|
|
31
|
+
|
|
32
|
+
expect(svg.hasClass(testClass)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders children inside svg', () => {
|
|
36
|
+
const d = 'M10 10L20 20';
|
|
37
|
+
const cx = 15;
|
|
38
|
+
const cy = 15;
|
|
39
|
+
const r = 5;
|
|
40
|
+
const wrapper = mount(
|
|
41
|
+
<SvgElement>
|
|
42
|
+
<path d={d} />
|
|
43
|
+
<circle cx={cx} cy={cy} r={r} />
|
|
44
|
+
</SvgElement>,
|
|
45
|
+
);
|
|
46
|
+
const circle = wrapper.find('circle');
|
|
47
|
+
|
|
48
|
+
expect(wrapper.find('path')).toHaveLength(1);
|
|
49
|
+
expect(wrapper.find('circle')).toHaveLength(1);
|
|
50
|
+
expect(wrapper.find('path').prop('d')).toBe(d);
|
|
51
|
+
expect(circle.prop('cx')).toBe(cx);
|
|
52
|
+
expect(circle.prop('cy')).toBe(cy);
|
|
53
|
+
expect(circle.prop('r')).toBe(r);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders as decorative icon by default', () => {
|
|
57
|
+
const wrapper = mount(<SvgElement />);
|
|
58
|
+
const svg = wrapper.find('svg');
|
|
59
|
+
|
|
60
|
+
expect(svg.prop('role')).toBe('presentation');
|
|
61
|
+
expect(svg.prop('aria-hidden')).toBe('true');
|
|
62
|
+
expect(svg.prop('aria-label')).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders as semantic icon when aria-label is provided', () => {
|
|
66
|
+
const ariaLabel = 'Custom icon description';
|
|
67
|
+
const wrapper = mount(<SvgElement aria-label={ariaLabel} />);
|
|
68
|
+
const svg = wrapper.find('svg');
|
|
69
|
+
|
|
70
|
+
expect(svg.prop('role')).toBe('img');
|
|
71
|
+
expect(svg.prop('aria-hidden')).toBeUndefined();
|
|
72
|
+
expect(svg.prop('aria-label')).toBe(ariaLabel);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders as semantic icon when title is provided', () => {
|
|
76
|
+
const title = 'Icon tooltip';
|
|
77
|
+
const wrapper = mount(<SvgElement title={title} />);
|
|
78
|
+
const svg = wrapper.find('svg');
|
|
79
|
+
|
|
80
|
+
expect(svg.prop('role')).toBe('img');
|
|
81
|
+
expect(svg.prop('aria-hidden')).toBeUndefined();
|
|
82
|
+
expect(wrapper.find('title')).toHaveLength(1);
|
|
83
|
+
expect(wrapper.find('title').text()).toBe(title);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders as semantic icon when both aria-label and title are provided', () => {
|
|
87
|
+
const ariaLabel = 'Icon description';
|
|
88
|
+
const title = 'Icon tooltip';
|
|
89
|
+
const wrapper = mount(<SvgElement aria-label={ariaLabel} title={title} />);
|
|
90
|
+
const svg = wrapper.find('svg');
|
|
91
|
+
|
|
92
|
+
expect(svg.prop('role')).toBe('img');
|
|
93
|
+
expect(svg.prop('aria-hidden')).toBeUndefined();
|
|
94
|
+
expect(svg.prop('aria-label')).toBe(ariaLabel);
|
|
95
|
+
expect(wrapper.find('title')).toHaveLength(1);
|
|
96
|
+
expect(wrapper.find('title').text()).toBe(title);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('does not render title element when title prop is not provided', () => {
|
|
100
|
+
const wrapper = mount(<SvgElement aria-label="Icon" />);
|
|
101
|
+
|
|
102
|
+
expect(wrapper.find('title')).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('applies default SVG attributes', () => {
|
|
106
|
+
const wrapper = mount(<SvgElement />);
|
|
107
|
+
const svg = wrapper.find('svg');
|
|
108
|
+
|
|
109
|
+
expect(svg.prop('focusable')).toBe('false');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('passes through additional SVG attributes', () => {
|
|
113
|
+
const customProps: SvgElementProps = {
|
|
114
|
+
id: 'custom-id',
|
|
115
|
+
'data-testid': 'custom-svg',
|
|
116
|
+
fill: 'currentColor',
|
|
117
|
+
stroke: 'red',
|
|
118
|
+
strokeWidth: '2',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const wrapper = mount(<SvgElement {...customProps} />);
|
|
122
|
+
const svg = wrapper.find('svg');
|
|
123
|
+
|
|
124
|
+
expect(svg.prop('id')).toBe('custom-id');
|
|
125
|
+
expect(svg.prop('data-testid')).toBe('custom-svg');
|
|
126
|
+
expect(svg.prop('fill')).toBe('currentColor');
|
|
127
|
+
expect(svg.prop('stroke')).toBe('red');
|
|
128
|
+
expect(svg.prop('strokeWidth')).toBe('2');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('allows overriding default attributes', () => {
|
|
132
|
+
const wrapper = mount(
|
|
133
|
+
<SvgElement
|
|
134
|
+
version="2.0"
|
|
135
|
+
xmlns="http://custom-namespace.com"
|
|
136
|
+
focusable="true"
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
const svg = wrapper.find('svg');
|
|
140
|
+
|
|
141
|
+
expect(svg.prop('version')).toBe('2.0');
|
|
142
|
+
expect(svg.prop('xmlns')).toBe('http://custom-namespace.com');
|
|
143
|
+
expect(svg.prop('focusable')).toBe('true');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('prioritizes explicit role over calculated role', () => {
|
|
147
|
+
const wrapper = mount(<SvgElement role="button" aria-label="Click me" />);
|
|
148
|
+
const svg = wrapper.find('svg');
|
|
149
|
+
|
|
150
|
+
expect(svg.prop('role')).toBe('button');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('prioritizes explicit aria-hidden over calculated value', () => {
|
|
154
|
+
const wrapper = mount(<SvgElement aria-hidden="false" />);
|
|
155
|
+
const svg = wrapper.find('svg');
|
|
156
|
+
|
|
157
|
+
expect(svg.prop('aria-hidden')).toBe('false');
|
|
158
|
+
expect(svg.prop('role')).toBe('presentation'); // Still decorative without label/title
|
|
159
|
+
});
|
|
160
|
+
});
|