@axinom/mosaic-ui 0.66.0-rc.9 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/DynamicDataList/DynamicListHeader/DynamicListHeader.d.ts.map +1 -1
- package/dist/components/FieldSelection/FieldSelection.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/DateTimeField/DateTimeText.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/ListCheckBox/ListCheckBox.d.ts.map +1 -1
- package/dist/components/List/ListHeader/ColumnLabel/ColumnLabel.d.ts.map +1 -1
- package/dist/components/List/ListHeader/ListHeader.d.ts.map +1 -1
- package/dist/components/List/ListRow/ListRow.d.ts.map +1 -1
- package/dist/components/List/ListRow/ListRowCell/ListRowCell.d.ts +15 -0
- package/dist/components/List/ListRow/ListRowCell/ListRowCell.d.ts.map +1 -0
- package/dist/components/List/ListRow/ListRowCell/renderData.d.ts +9 -0
- package/dist/components/List/ListRow/ListRowCell/renderData.d.ts.map +1 -0
- package/dist/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.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/Utils/Postgraphile/CreateConnectionRenderer.d.ts +1 -1
- package/dist/components/Utils/Postgraphile/CreateConnectionRenderer.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/helpers/idleCallbackHelpers.d.ts +42 -0
- package/dist/helpers/idleCallbackHelpers.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/hooks/useResize/ResizeIndicator.d.ts +8 -0
- package/dist/hooks/useResize/ResizeIndicator.d.ts.map +1 -0
- package/dist/hooks/useResize/useResize.d.ts +5 -2
- package/dist/hooks/useResize/useResize.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/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/Accordion/Accordion.scss +1 -1
- package/src/components/Accordion/AccordionItem/AccordionItem.scss +2 -2
- package/src/components/Buttons/Button/Button.scss +5 -5
- package/src/components/Buttons/CompositeButton/CompositeButton.scss +7 -7
- package/src/components/Buttons/TextButton/TextButton.scss +6 -6
- package/src/components/ConfirmDialog/ConfirmDialog.scss +1 -1
- package/src/components/DateTime/DatePicker/DatePicker.scss +12 -12
- package/src/components/DateTime/TimePicker/ScrollColumn/ScrollColumn.scss +7 -7
- package/src/components/DateTime/TimePicker/TimePicker.scss +1 -1
- package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.scss +2 -2
- package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.spec.tsx +2 -0
- package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.tsx +62 -50
- package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.scss +2 -2
- package/src/components/FieldSelection/FieldSelection.scss +6 -0
- package/src/components/FieldSelection/FieldSelection.tsx +2 -1
- package/src/components/Filters/Filter/Filter.scss +34 -15
- package/src/components/Filters/Filter/Filter.spec.tsx +1 -1
- package/src/components/Filters/Filter/Filter.tsx +46 -34
- 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.scss +1 -1
- 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.scss +6 -11
- package/src/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.tsx +8 -0
- package/src/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.tsx +6 -1
- package/src/components/FormElements/Checkbox/Checkbox.scss +4 -4
- package/src/components/FormElements/CustomTags/CustomTags.scss +1 -1
- package/src/components/FormElements/DateTimeField/DateTimeText.scss +5 -6
- package/src/components/FormElements/DateTimeField/DateTimeText.tsx +58 -43
- package/src/components/FormElements/FileUploadControl/FileUploadControl.scss +4 -4
- package/src/components/FormElements/Radio/Radio.scss +5 -5
- package/src/components/FormElements/Radio/Radio.tsx +3 -2
- package/src/components/FormElements/Select/Select.scss +11 -6
- package/src/components/FormElements/ToggleButton/ToggleButton.scss +7 -7
- 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/InfoTooltip/InfoTooltip.scss +1 -1
- package/src/components/InlineMenu/InlineMenu.scss +2 -2
- 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/List.scss +4 -4
- package/src/components/List/ListCheckBox/ListCheckBox.tsx +1 -0
- package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.spec.tsx +6 -6
- package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.tsx +10 -13
- package/src/components/List/ListHeader/ListHeader.scss +2 -3
- package/src/components/List/ListHeader/ListHeader.spec.tsx +2 -0
- package/src/components/List/ListHeader/ListHeader.tsx +57 -51
- package/src/components/List/ListRow/ListRow.scss +1 -28
- package/src/components/List/ListRow/ListRow.spec.tsx +10 -8
- package/src/components/List/ListRow/ListRow.tsx +20 -152
- package/src/components/List/ListRow/ListRowCell/ListRowCell.scss +26 -0
- package/src/components/List/ListRow/ListRowCell/ListRowCell.spec.tsx +491 -0
- package/src/components/List/ListRow/ListRowCell/ListRowCell.tsx +57 -0
- package/src/components/List/ListRow/ListRowCell/renderData.tsx +124 -0
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.spec.tsx +191 -80
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.tsx +63 -34
- package/src/components/Loaders/ImageLoader/ImageLoader.spec.tsx +13 -14
- package/src/components/Loaders/ImageLoader/ImageLoader.tsx +5 -3
- package/src/components/PageHeader/PageHeader.scss +1 -1
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +13 -2
- package/src/components/Tabs/Tab/CustomTab.scss +4 -4
- package/src/components/Tabs/TabList/CustomTabList.scss +2 -2
- package/src/components/Utils/Postgraphile/CreateConnectionRenderer.spec.ts +48 -12
- package/src/components/Utils/Postgraphile/CreateConnectionRenderer.tsx +5 -4
- package/src/components/VisualElements/ImgElement.spec.tsx +92 -0
- package/src/components/VisualElements/ImgElement.tsx +72 -0
- package/src/components/VisualElements/SvgElement.spec.tsx +158 -0
- package/src/components/VisualElements/SvgElement.tsx +40 -0
- package/src/components/VisualElements/index.ts +7 -0
- package/src/components/index.ts +1 -0
- package/src/helpers/idleCallbackHelpers.ts +66 -0
- package/src/helpers/index.ts +5 -0
- package/src/hooks/useResize/ResizeIndicator.scss +7 -0
- package/src/hooks/useResize/ResizeIndicator.tsx +39 -0
- package/src/hooks/useResize/{useResize.ts → useResize.tsx} +38 -6
- package/src/index.ts +2 -0
- package/src/styles/branding.scss +89 -0
- package/src/styles/variables.scss +245 -187
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
display: grid;
|
|
7
7
|
height: 50px;
|
|
8
|
-
color:
|
|
8
|
+
color: var(--ax-primary);
|
|
9
9
|
font-size: 14px;
|
|
10
10
|
font-weight: bold;
|
|
11
11
|
border-bottom: none;
|
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
|
|
18
18
|
&.selected {
|
|
19
19
|
background: white;
|
|
20
|
-
color:
|
|
20
|
+
color: var(--ax-primary-dark);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
&:hover:not(.selected):not(.disabled) {
|
|
24
|
-
background-color:
|
|
24
|
+
background-color: var(--ax-primary);
|
|
25
25
|
transition: background-color $confirmation-transition linear;
|
|
26
26
|
color: white;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
&.disabled {
|
|
30
|
-
color:
|
|
30
|
+
color: var(--ax-neutral);
|
|
31
31
|
cursor: default;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -20,7 +20,10 @@ describe('createConnectionRenderer', () => {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const selector = (item: { id: string; name: string }) => item.name;
|
|
23
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
23
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
24
|
+
selector,
|
|
25
|
+
false,
|
|
26
|
+
);
|
|
24
27
|
const result = renderer(connection);
|
|
25
28
|
|
|
26
29
|
expect(result).toBe('Item 1, Item 2, Item 3');
|
|
@@ -32,7 +35,10 @@ describe('createConnectionRenderer', () => {
|
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
const selector = (item: string) => item;
|
|
35
|
-
const renderer = createConnectionRenderer<StringConnection>(
|
|
38
|
+
const renderer = createConnectionRenderer<StringConnection>(
|
|
39
|
+
selector,
|
|
40
|
+
false,
|
|
41
|
+
);
|
|
36
42
|
const result = renderer(connection);
|
|
37
43
|
|
|
38
44
|
expect(result).toBe('apple, banana, cherry');
|
|
@@ -48,7 +54,10 @@ describe('createConnectionRenderer', () => {
|
|
|
48
54
|
|
|
49
55
|
const selector = (item: { id: string; name: string }, index: number) =>
|
|
50
56
|
`${index + 1}. ${item.name} (${item.id})`;
|
|
51
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
57
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
58
|
+
selector,
|
|
59
|
+
false,
|
|
60
|
+
);
|
|
52
61
|
const result = renderer(connection);
|
|
53
62
|
|
|
54
63
|
expect(result).toBe('1. Item 1 (1), 2. Item 2 (2)');
|
|
@@ -57,7 +66,10 @@ describe('createConnectionRenderer', () => {
|
|
|
57
66
|
it('should return empty string when nodes array is empty', () => {
|
|
58
67
|
const connection: TestConnection = { nodes: [] };
|
|
59
68
|
const selector = (item: { id: string; name: string }) => item.name;
|
|
60
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
69
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
70
|
+
selector,
|
|
71
|
+
false,
|
|
72
|
+
);
|
|
61
73
|
const result = renderer(connection);
|
|
62
74
|
|
|
63
75
|
expect(result).toBe('');
|
|
@@ -75,7 +87,10 @@ describe('createConnectionRenderer', () => {
|
|
|
75
87
|
};
|
|
76
88
|
|
|
77
89
|
const selector = (item: { id: number; value: number }) => item.value;
|
|
78
|
-
const renderer = createConnectionRenderer<NumberConnection>(
|
|
90
|
+
const renderer = createConnectionRenderer<NumberConnection>(
|
|
91
|
+
selector,
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
79
94
|
const result = renderer(connection);
|
|
80
95
|
|
|
81
96
|
expect(result).toBe('100, 200');
|
|
@@ -84,21 +99,30 @@ describe('createConnectionRenderer', () => {
|
|
|
84
99
|
it('should throw error when connection.nodes is undefined', () => {
|
|
85
100
|
const connection = { nodes: undefined } as unknown as TestConnection;
|
|
86
101
|
const selector = (item: { id: string; name: string }) => item.name;
|
|
87
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
102
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
103
|
+
selector,
|
|
104
|
+
false,
|
|
105
|
+
);
|
|
88
106
|
|
|
89
107
|
expect(() => renderer(connection)).toThrow();
|
|
90
108
|
});
|
|
91
109
|
|
|
92
110
|
it('should throw error when value is null', () => {
|
|
93
111
|
const selector = (item: { id: string; name: string }) => item.name;
|
|
94
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
112
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
113
|
+
selector,
|
|
114
|
+
false,
|
|
115
|
+
);
|
|
95
116
|
|
|
96
117
|
expect(() => renderer(null)).toThrow();
|
|
97
118
|
});
|
|
98
119
|
|
|
99
120
|
it('should throw error when value is undefined', () => {
|
|
100
121
|
const selector = (item: { id: string; name: string }) => item.name;
|
|
101
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
122
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
123
|
+
selector,
|
|
124
|
+
false,
|
|
125
|
+
);
|
|
102
126
|
|
|
103
127
|
expect(() => renderer(undefined)).toThrow();
|
|
104
128
|
});
|
|
@@ -111,7 +135,10 @@ describe('createConnectionRenderer', () => {
|
|
|
111
135
|
};
|
|
112
136
|
|
|
113
137
|
const selector = (item: string) => item;
|
|
114
|
-
const renderer = createConnectionRenderer<StringConnection>(
|
|
138
|
+
const renderer = createConnectionRenderer<StringConnection>(
|
|
139
|
+
selector,
|
|
140
|
+
false,
|
|
141
|
+
);
|
|
115
142
|
const result = renderer(connection);
|
|
116
143
|
|
|
117
144
|
expect(result).toBe('item, with comma, item with "quotes", normal item');
|
|
@@ -127,7 +154,10 @@ describe('createConnectionRenderer', () => {
|
|
|
127
154
|
};
|
|
128
155
|
|
|
129
156
|
const selector = (item: { id: string; name: string }) => item.name;
|
|
130
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
157
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
158
|
+
selector,
|
|
159
|
+
false,
|
|
160
|
+
);
|
|
131
161
|
const result = renderer(connection);
|
|
132
162
|
|
|
133
163
|
expect(result).toBe(', Item 2, ');
|
|
@@ -139,7 +169,10 @@ describe('createConnectionRenderer', () => {
|
|
|
139
169
|
};
|
|
140
170
|
|
|
141
171
|
const selector = (item: { id: string; name: string }) => item.name;
|
|
142
|
-
const renderer = createConnectionRenderer<TestConnection>(
|
|
172
|
+
const renderer = createConnectionRenderer<TestConnection>(
|
|
173
|
+
selector,
|
|
174
|
+
false,
|
|
175
|
+
);
|
|
143
176
|
const result = renderer(connection);
|
|
144
177
|
|
|
145
178
|
expect(result).toBe('Single Item');
|
|
@@ -161,7 +194,10 @@ describe('createConnectionRenderer', () => {
|
|
|
161
194
|
|
|
162
195
|
const selector = (item: { customField: number; label: string }) =>
|
|
163
196
|
`${item.label}: ${item.customField}`;
|
|
164
|
-
const renderer = createConnectionRenderer<CustomConnection>(
|
|
197
|
+
const renderer = createConnectionRenderer<CustomConnection>(
|
|
198
|
+
selector,
|
|
199
|
+
false,
|
|
200
|
+
);
|
|
165
201
|
const result = renderer(connection);
|
|
166
202
|
|
|
167
203
|
expect(result).toBe('Custom 1: 42, Custom 2: 84');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
+
import { TagsRenderer } from '../../List';
|
|
2
3
|
|
|
3
4
|
interface Connection {
|
|
4
5
|
nodes: unknown[];
|
|
@@ -18,14 +19,14 @@ export type SelectorFunction<T> = (
|
|
|
18
19
|
*/
|
|
19
20
|
export function createConnectionRenderer<T extends Connection>(
|
|
20
21
|
selector: SelectorFunction<T['nodes'][number]>,
|
|
21
|
-
|
|
22
|
+
renderAsTags = true,
|
|
22
23
|
): (val: unknown) => string | ReactNode {
|
|
23
24
|
const ConnectionRenderer = (val: unknown): string | ReactNode => {
|
|
24
25
|
const value = val as T;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if (renderAsTags) {
|
|
28
|
+
return TagsRenderer(value.nodes.map(selector));
|
|
29
|
+
}
|
|
29
30
|
|
|
30
31
|
return value.nodes.map(selector).join(', ');
|
|
31
32
|
};
|
|
@@ -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,158 @@
|
|
|
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
|
+
fill: 'currentColor',
|
|
116
|
+
stroke: 'red',
|
|
117
|
+
strokeWidth: '2',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const wrapper = mount(<SvgElement {...customProps} />);
|
|
121
|
+
const svg = wrapper.find('svg');
|
|
122
|
+
|
|
123
|
+
expect(svg.prop('id')).toBe('custom-id');
|
|
124
|
+
expect(svg.prop('fill')).toBe('currentColor');
|
|
125
|
+
expect(svg.prop('stroke')).toBe('red');
|
|
126
|
+
expect(svg.prop('strokeWidth')).toBe('2');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('allows overriding default attributes', () => {
|
|
130
|
+
const wrapper = mount(
|
|
131
|
+
<SvgElement
|
|
132
|
+
version="2.0"
|
|
133
|
+
xmlns="http://custom-namespace.com"
|
|
134
|
+
focusable="true"
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
const svg = wrapper.find('svg');
|
|
138
|
+
|
|
139
|
+
expect(svg.prop('version')).toBe('2.0');
|
|
140
|
+
expect(svg.prop('xmlns')).toBe('http://custom-namespace.com');
|
|
141
|
+
expect(svg.prop('focusable')).toBe('true');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('prioritizes explicit role over calculated role', () => {
|
|
145
|
+
const wrapper = mount(<SvgElement role="button" aria-label="Click me" />);
|
|
146
|
+
const svg = wrapper.find('svg');
|
|
147
|
+
|
|
148
|
+
expect(svg.prop('role')).toBe('button');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('prioritizes explicit aria-hidden over calculated value', () => {
|
|
152
|
+
const wrapper = mount(<SvgElement aria-hidden="false" />);
|
|
153
|
+
const svg = wrapper.find('svg');
|
|
154
|
+
|
|
155
|
+
expect(svg.prop('aria-hidden')).toBe('false');
|
|
156
|
+
expect(svg.prop('role')).toBe('presentation'); // Still decorative without label/title
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SvgElementProps extends React.SVGAttributes<SVGElement> {
|
|
4
|
+
/** SVG title (tooltip or additional description shown on hover) */
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reusable SVG wrapper with accessibility support and default props.
|
|
10
|
+
* @example
|
|
11
|
+
* <SvgElement aria-label="Custom icon" title="Custom tooltip">
|
|
12
|
+
* <path d="..." />
|
|
13
|
+
* </SvgElement>
|
|
14
|
+
*/
|
|
15
|
+
export const SvgElement: React.FC<SvgElementProps> = ({
|
|
16
|
+
children,
|
|
17
|
+
className,
|
|
18
|
+
'aria-label': ariaLabel,
|
|
19
|
+
title,
|
|
20
|
+
viewBox = '0 0 40 40',
|
|
21
|
+
...svgProps
|
|
22
|
+
}) => {
|
|
23
|
+
// Determine if this is a decorative icon (no accessible label or title)
|
|
24
|
+
const isDecorative = !ariaLabel && !title;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<svg
|
|
28
|
+
className={className}
|
|
29
|
+
viewBox={viewBox}
|
|
30
|
+
role={isDecorative ? 'presentation' : 'img'}
|
|
31
|
+
aria-hidden={isDecorative ? 'true' : undefined}
|
|
32
|
+
aria-label={ariaLabel}
|
|
33
|
+
focusable="false"
|
|
34
|
+
{...svgProps}
|
|
35
|
+
>
|
|
36
|
+
{title && <title>{title}</title>}
|
|
37
|
+
{children}
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type for the callback handle returned by scheduleIdleCallback.
|
|
3
|
+
* Can be either a number (from requestIdleCallback) or a NodeJS.Timeout (from setTimeout).
|
|
4
|
+
*/
|
|
5
|
+
export type IdleCallbackHandle = number | NodeJS.Timeout;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Safely schedules a callback to be executed when the browser is idle.
|
|
9
|
+
* Falls back to setTimeout if requestIdleCallback is not available.
|
|
10
|
+
*
|
|
11
|
+
* @param callback - The function to execute during idle time
|
|
12
|
+
* @param options - Optional configuration (timeout in ms)
|
|
13
|
+
* @returns A handle that can be passed to cancelScheduledCallback
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const handle = scheduleIdleCallback(() => {
|
|
18
|
+
* console.log('Running during idle time');
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Later, if needed:
|
|
22
|
+
* cancelScheduledCallback(handle);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function scheduleIdleCallback(
|
|
26
|
+
callback: IdleRequestCallback,
|
|
27
|
+
options?: IdleRequestOptions,
|
|
28
|
+
): IdleCallbackHandle {
|
|
29
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
30
|
+
return requestIdleCallback(callback, options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback to setTimeout for browsers that don't support requestIdleCallback
|
|
34
|
+
// Use a short timeout to approximate idle behavior
|
|
35
|
+
const timeout = options?.timeout ?? 1;
|
|
36
|
+
return setTimeout(() => {
|
|
37
|
+
const deadline: IdleDeadline = {
|
|
38
|
+
didTimeout: false,
|
|
39
|
+
timeRemaining: () => 50, // Approximate 50ms of available time
|
|
40
|
+
};
|
|
41
|
+
callback(deadline);
|
|
42
|
+
}, timeout);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cancels a callback scheduled with scheduleIdleCallback.
|
|
47
|
+
* Works with both requestIdleCallback and setTimeout fallback.
|
|
48
|
+
*
|
|
49
|
+
* @param handle - The handle returned by scheduleIdleCallback
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const handle = scheduleIdleCallback(() => {
|
|
54
|
+
* console.log('This might not run');
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* cancelScheduledCallback(handle);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function cancelScheduledCallback(handle: IdleCallbackHandle): void {
|
|
61
|
+
if (typeof cancelIdleCallback !== 'undefined' && typeof handle === 'number') {
|
|
62
|
+
cancelIdleCallback(handle);
|
|
63
|
+
} else {
|
|
64
|
+
clearTimeout(handle as NodeJS.Timeout);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/helpers/index.ts
CHANGED