@scality/core-ui 0.162.0 → 0.164.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.
Files changed (73) hide show
  1. package/dist/components/barchartv2/Barchart.component.d.ts +9 -3
  2. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  3. package/dist/components/barchartv2/Barchart.component.js +22 -5
  4. package/dist/components/barchartv2/utils.d.ts +26 -3
  5. package/dist/components/barchartv2/utils.d.ts.map +1 -1
  6. package/dist/components/barchartv2/utils.js +76 -22
  7. package/dist/components/chartlegend/ChartLegend.d.ts +8 -0
  8. package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -0
  9. package/dist/components/chartlegend/ChartLegend.js +65 -0
  10. package/dist/components/chartlegend/ChartLegendWrapper.d.ts +17 -0
  11. package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -0
  12. package/dist/components/chartlegend/ChartLegendWrapper.js +50 -0
  13. package/dist/components/date/FormattedDateTime.d.ts +3 -1
  14. package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
  15. package/dist/components/date/FormattedDateTime.js +19 -1
  16. package/dist/components/date/FormattedDateTime.spec.js +12 -0
  17. package/dist/components/icon/Icon.component.d.ts +5 -5
  18. package/dist/components/icon/Icon.component.d.ts.map +1 -1
  19. package/dist/components/icon/Icon.component.js +33 -31
  20. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +33 -0
  21. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -0
  22. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +249 -0
  23. package/dist/components/selectv2/Selectv2.component.d.ts.map +1 -1
  24. package/dist/components/selectv2/Selectv2.component.js +11 -6
  25. package/dist/components/steppers/Stepper.component.d.ts.map +1 -1
  26. package/dist/components/steppers/Stepper.component.js +9 -8
  27. package/dist/components/toast/ToastProvider.d.ts.map +1 -1
  28. package/dist/components/toast/ToastProvider.js +4 -5
  29. package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +1 -1
  30. package/dist/components/vegachartv2/SyncedCursorCharts.js +3 -5
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/next.d.ts +2 -0
  35. package/dist/next.d.ts.map +1 -1
  36. package/dist/next.js +2 -0
  37. package/dist/style/theme.d.ts +1 -0
  38. package/dist/style/theme.d.ts.map +1 -1
  39. package/dist/style/theme.js +28 -0
  40. package/package.json +2 -2
  41. package/src/lib/components/accordion/Accordion.test.tsx +8 -16
  42. package/src/lib/components/barchartv2/Barchart.component.test.tsx +117 -111
  43. package/src/lib/components/barchartv2/Barchart.component.tsx +54 -7
  44. package/src/lib/components/barchartv2/utils.test.ts +127 -2
  45. package/src/lib/components/barchartv2/utils.ts +103 -19
  46. package/src/lib/components/chartlegend/ChartLegend.tsx +113 -0
  47. package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +85 -0
  48. package/src/lib/components/date/FormattedDateTime.spec.tsx +24 -0
  49. package/src/lib/components/date/FormattedDateTime.tsx +36 -2
  50. package/src/lib/components/healthselectorv2/HealthSelector.component.test.tsx +10 -10
  51. package/src/lib/components/icon/Icon.component.tsx +48 -60
  52. package/src/lib/components/infomessage/InfoMessageUtils.test.tsx +0 -1
  53. package/src/lib/components/inlineinput/InlineInput.test.tsx +28 -22
  54. package/src/lib/components/inputlist/InputList.test.tsx +22 -21
  55. package/src/lib/components/linetemporalchart/ChartUtil.test.ts +5 -4
  56. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +502 -0
  57. package/src/lib/components/searchinput/SearchInput.test.tsx +3 -7
  58. package/src/lib/components/selectv2/Selectv2.component.tsx +13 -5
  59. package/src/lib/components/selectv2/selectv2.test.tsx +70 -61
  60. package/src/lib/components/steppers/Stepper.component.tsx +10 -8
  61. package/src/lib/components/tablev2/TableSync.test.tsx +8 -12
  62. package/src/lib/components/tablev2/TableUtils.test.ts +6 -3
  63. package/src/lib/components/tablev2/Tablev2.test.tsx +38 -40
  64. package/src/lib/components/toast/ToastProvider.tsx +14 -6
  65. package/src/lib/components/toggle/Toggle.test.tsx +1 -1
  66. package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +5 -7
  67. package/src/lib/index.ts +1 -0
  68. package/src/lib/next.ts +2 -0
  69. package/src/lib/style/theme.ts +29 -0
  70. package/stories/BarChart/barchart.stories.tsx +387 -129
  71. package/stories/format.mdx +4 -2
  72. package/stories/linetimeseriechart.stories.tsx +485 -0
  73. package/tsconfig.json +0 -1
@@ -0,0 +1,502 @@
1
+ import {
2
+ Line,
3
+ LineChart,
4
+ ReferenceLine,
5
+ ResponsiveContainer,
6
+ Tooltip,
7
+ XAxis,
8
+ YAxis,
9
+ CartesianGrid,
10
+ } from 'recharts';
11
+ import { useMemo, useRef } from 'react';
12
+ import { useTheme } from 'styled-components';
13
+ import { useMetricsTimeSpan } from '../linetemporalchart/MetricTimespanProvider';
14
+ import { addMissingDataPoint } from '../linetemporalchart/ChartUtil';
15
+ import styled from 'styled-components';
16
+ import { fontSize, fontWeight } from '../../style/theme';
17
+ import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
18
+ import { ChartTitleText, SmallerText } from '../text/Text.component';
19
+ import { Loader } from '../loader/Loader.component';
20
+ import { spacing } from '../../spacing';
21
+ import { getUnitLabel } from '../linetemporalchart/ChartUtil';
22
+ import { Icon } from '../icon/Icon.component';
23
+ import { Tooltip as TooltipComponent } from '../tooltip/Tooltip.component';
24
+ import {
25
+ DAY_MONTH_ABBREVIATED_HOUR_MINUTE,
26
+ FormattedDateTime,
27
+ } from '../date/FormattedDateTime';
28
+
29
+ const LineTemporalChartWrapper = styled.div`
30
+ display: flex;
31
+ flex-direction: column;
32
+ justify-content: flex-start;
33
+ flex: 1;
34
+ `;
35
+
36
+ const ChartHeader = styled.div`
37
+ display: flex;
38
+ align-items: center;
39
+ `;
40
+
41
+ const TooltipContainer = styled.div`
42
+ background-color: ${(props) => props.theme.backgroundLevel1};
43
+ padding: ${spacing.r8};
44
+ border: 1px solid ${(props) => props.theme.border};
45
+ border-radius: 4px;
46
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
47
+ max-width: 250px;
48
+ `;
49
+
50
+ const TooltipTime = styled.div`
51
+ margin-bottom: ${spacing.r8};
52
+ color: ${(props) => props.theme.textPrimary};
53
+ font-size: ${fontSize.smaller};
54
+ font-weight: ${fontWeight.bold};
55
+ text-align: center;
56
+ `;
57
+
58
+ const TooltipValue = styled.div`
59
+ font-size: ${fontSize.smaller};
60
+ margin-top: 4px;
61
+ color: ${(props) => props.theme.textSecondary};
62
+ display: flex;
63
+ align-items: flex-start;
64
+ `;
65
+
66
+ const TooltipLegend = styled.div<{ color: string }>`
67
+ width: 12px;
68
+ height: 3px;
69
+ background-color: ${(props) => props.color};
70
+ margin-right: 8px;
71
+ flex-shrink: 0;
72
+ margin-top: 8px;
73
+ `;
74
+
75
+ const TooltipContent = styled.div`
76
+ display: flex;
77
+ min-width: 0;
78
+ flex: 1;
79
+ `;
80
+
81
+ const TooltipName = styled.div`
82
+ margin-right: 4px;
83
+ word-wrap: break-word;
84
+ word-break: break-word;
85
+ justify-content: flex-start;
86
+ `;
87
+
88
+ const TooltipInstanceValue = styled.div`
89
+ justify-content: flex-end;
90
+ `;
91
+
92
+ export type Serie = {
93
+ // the name of the resource
94
+ resource: string;
95
+ // the original data format from prometheus
96
+ data: [number, string | null][];
97
+ // it's mandatory to display tooltip label in the tooltip
98
+ getTooltipLabel: (metricPrefix?: string, resource?: string) => string;
99
+ // the name of the metric prefix with read, write, in, out
100
+ metricPrefix?: string;
101
+ // to specify if the line is dash
102
+ isLineDashed?: boolean;
103
+ };
104
+
105
+ type NonSymmetricalChartSerie = {
106
+ yAxisType?: 'default' | 'percentage';
107
+ series: Serie[];
108
+ };
109
+
110
+ // The symmetrical chart props are used to display two series on the same chart, such as in/out, write/read
111
+ type SymmetricalChartSerie = {
112
+ yAxisType: 'symmetrical';
113
+ series: {
114
+ above: Serie[];
115
+ below: Serie[];
116
+ };
117
+ };
118
+
119
+ export type LineChartProps = (
120
+ | NonSymmetricalChartSerie
121
+ | SymmetricalChartSerie
122
+ ) & {
123
+ title: string;
124
+ height: number;
125
+ startingTimeStamp: number;
126
+ unitRange?: {
127
+ threshold: number;
128
+ label: string;
129
+ }[];
130
+ isLoading?: boolean;
131
+ yAxisTitle?: string;
132
+ helpText?: string;
133
+ };
134
+
135
+ const CustomTooltip = ({
136
+ active,
137
+ payload,
138
+ label,
139
+ unitLabel,
140
+ }: {
141
+ active?: boolean;
142
+ payload?: Array<{
143
+ value: number;
144
+ name: string;
145
+ color: string;
146
+ dataKey: string;
147
+ }>;
148
+ label?: string;
149
+ unitLabel?: string;
150
+ }) => {
151
+ if (!active || !payload || !payload.length || !label) return null;
152
+ // We can't use the default itemSorter method because it's a custom tooltip.
153
+ // Sort the payload here instead
154
+ const sortedPayload = [...payload].sort((a, b) => {
155
+ const aValue = Number(a.value);
156
+ const bValue = Number(b.value);
157
+
158
+ if (aValue >= 0 && bValue >= 0) {
159
+ return bValue - aValue; // Higher positive values first
160
+ }
161
+ if (aValue < 0 && bValue < 0) {
162
+ return bValue - aValue; // Lower negative values first
163
+ }
164
+ return bValue - aValue; // Positives before negatives
165
+ });
166
+
167
+ return (
168
+ <TooltipContainer>
169
+ <TooltipTime>
170
+ <FormattedDateTime
171
+ format="day-month-abbreviated-hour-minute-second"
172
+ value={new Date(label)}
173
+ />
174
+ </TooltipTime>
175
+ {sortedPayload.map((entry, index) => (
176
+ <TooltipValue key={index}>
177
+ <TooltipLegend color={entry.color} />
178
+ <TooltipContent>
179
+ <TooltipName>{entry.name}</TooltipName>
180
+ <TooltipInstanceValue>
181
+ {isNaN(Number(entry.value))
182
+ ? '-'
183
+ : `${Number(entry.value).toFixed(2)}${unitLabel}`}
184
+ </TooltipInstanceValue>
185
+ </TooltipContent>
186
+ </TooltipValue>
187
+ ))}
188
+ </TooltipContainer>
189
+ );
190
+ };
191
+
192
+ const isSymmetricalSeries = (
193
+ series: Serie[] | { above: Serie[]; below: Serie[] },
194
+ ): series is { above: Serie[]; below: Serie[] } => {
195
+ return 'above' in series && 'below' in series;
196
+ };
197
+
198
+ export function LineTimeSerieChart({
199
+ series,
200
+ title,
201
+ height,
202
+ startingTimeStamp,
203
+ unitRange,
204
+ isLoading = false,
205
+ yAxisType = 'default',
206
+ yAxisTitle,
207
+ helpText,
208
+ ...rest
209
+ }: LineChartProps) {
210
+ const theme = useTheme();
211
+ const { frequency, duration } = useMetricsTimeSpan();
212
+ const { getColor } = useChartLegend();
213
+ const chartRef = useRef(null);
214
+
215
+ const chartData = useMemo(() => {
216
+ // 1. Add missing data points
217
+ const normalizedSeries =
218
+ yAxisType === 'symmetrical' && isSymmetricalSeries(series)
219
+ ? {
220
+ above: series.above.map((line) => ({
221
+ ...line,
222
+ data: addMissingDataPoint(
223
+ line.data,
224
+ startingTimeStamp,
225
+ duration,
226
+ frequency,
227
+ ),
228
+ })),
229
+ // Convert positive values to negative values
230
+ below: series.below.map((line) => ({
231
+ ...line,
232
+ data: addMissingDataPoint(
233
+ line.data,
234
+ startingTimeStamp,
235
+ duration,
236
+ frequency,
237
+ ).map(
238
+ ([timestamp, value]) =>
239
+ [timestamp, value === null ? null : `-${Number(value)}`] as [
240
+ number,
241
+ string | null,
242
+ ],
243
+ ),
244
+ })),
245
+ }
246
+ : (series as Serie[]).map((line) => ({
247
+ ...line,
248
+ data: addMissingDataPoint(
249
+ line.data,
250
+ startingTimeStamp,
251
+ duration,
252
+ frequency,
253
+ ),
254
+ }));
255
+
256
+ // 2. Convert directly to Recharts format
257
+ // Initialize an object to hold data points by timestamp
258
+ const dataPointsByTime: Record<
259
+ number,
260
+ { timestamp: number } & Record<string, string | number | null>
261
+ > = {};
262
+ const seriesToProcess =
263
+ yAxisType === 'symmetrical' && isSymmetricalSeries(normalizedSeries)
264
+ ? [...normalizedSeries.above, ...normalizedSeries.below]
265
+ : (normalizedSeries as Serie[]);
266
+
267
+ seriesToProcess.forEach((serie) => {
268
+ const label = serie.getTooltipLabel(serie.metricPrefix, serie.resource);
269
+
270
+ serie.data.forEach((point) => {
271
+ const timestamp =
272
+ typeof point[0] === 'number' ? point[0] * 1000 : Number(point[0]);
273
+ const value = point[1];
274
+ // Initialize this timestamp if it doesn't exist
275
+ if (!dataPointsByTime[timestamp]) {
276
+ dataPointsByTime[timestamp] = { timestamp };
277
+ }
278
+ // Add this metric's value to the data point, and convert the value to a number if it's a string
279
+ dataPointsByTime[timestamp][label] =
280
+ typeof value === 'string' ? Number(value) : value;
281
+ });
282
+ });
283
+ // Convert object to array for Recharts
284
+ return Object.values(dataPointsByTime).sort(
285
+ (
286
+ a: { timestamp: number } & Record<string, string | number | null>,
287
+ b: { timestamp: number } & Record<string, string | number | null>,
288
+ ) => (a.timestamp as number) - (b.timestamp as number),
289
+ );
290
+ }, [series, startingTimeStamp, duration, frequency, yAxisType]);
291
+
292
+ // Calculate 5 perfectly evenly spaced ticks
293
+ const xAxisTicks = useMemo(() => {
294
+ if (!chartData || chartData.length === 0) return [];
295
+
296
+ const timestamps: number[] = chartData.map((d) => d.timestamp);
297
+ const minTimestamp = Math.min(...timestamps);
298
+ const maxTimestamp = Math.max(...timestamps);
299
+
300
+ // Calculate 5 perfectly evenly spaced ticks
301
+ const timeRange = maxTimestamp - minTimestamp;
302
+ const interval = timeRange / 4; // 4 intervals create 5 points
303
+
304
+ const exactEvenTicks = [
305
+ minTimestamp,
306
+ minTimestamp + interval,
307
+ minTimestamp + interval * 2,
308
+ minTimestamp + interval * 3,
309
+ maxTimestamp,
310
+ ];
311
+
312
+ // Return perfectly even ticks (guaranteed to be evenly divided)
313
+ return exactEvenTicks;
314
+ }, [chartData]);
315
+
316
+ // 3. Transform the data base on the valuebase
317
+ const { topValue, unitLabel, rechartsData } = useMemo(() => {
318
+ if (yAxisType === 'percentage')
319
+ return {
320
+ topValue: 100,
321
+ unitLabel: '%',
322
+ rechartsData: chartData,
323
+ };
324
+
325
+ const values = chartData.flatMap((dataPoint) =>
326
+ Object.entries(dataPoint)
327
+ .filter(([key]) => key !== 'timestamp')
328
+ .map(([_, value]) => {
329
+ const num =
330
+ typeof value === 'string' ? Number(value) : (value ?? Infinity);
331
+ return !isNaN(num) && num !== null ? num : null;
332
+ })
333
+ .filter((value): value is number => value !== null),
334
+ );
335
+
336
+ const top = Math.abs(Math.max(...values));
337
+ const bottom = Math.abs(Math.min(...values));
338
+ const maxValue = Math.max(top, bottom);
339
+
340
+ const { valueBase, unitLabel } = getUnitLabel(unitRange ?? [], maxValue);
341
+
342
+ const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
343
+
344
+ const rechartsData = chartData.map((dataPoint) => {
345
+ const normalizedDataPoint = { ...dataPoint };
346
+ Object.entries(dataPoint).forEach(([key, value]) => {
347
+ if (key !== 'timestamp' && typeof value === 'number') {
348
+ normalizedDataPoint[key] = value / valueBase;
349
+ }
350
+ });
351
+ return normalizedDataPoint;
352
+ });
353
+
354
+ return { topValue, unitLabel, rechartsData };
355
+ }, [chartData, yAxisType, unitRange]);
356
+
357
+ // Group series by resource and create color mapping
358
+ const { colorMapping, groupedSeries } = useMemo(() => {
359
+ const mapping: Record<string, string> = {};
360
+ const allSeries = isSymmetricalSeries(series)
361
+ ? [...series.above, ...series.below]
362
+ : (series as Serie[]);
363
+
364
+ // Group series by resource
365
+ const groups = allSeries.reduce(
366
+ (acc, serie) => {
367
+ const key = serie.resource;
368
+ if (!acc[key]) {
369
+ acc[key] = [];
370
+ }
371
+ acc[key].push(serie);
372
+ return acc;
373
+ },
374
+ {} as Record<string, Serie[]>,
375
+ );
376
+
377
+ // Get colors from the ChartLegend context
378
+ Object.keys(groups).forEach((resource) => {
379
+ const color = getColor(resource);
380
+ if (color) {
381
+ mapping[resource] = color;
382
+ } else {
383
+ console.warn(`Color not defined for resource: ${resource}`);
384
+ }
385
+ });
386
+
387
+ return {
388
+ colorMapping: mapping,
389
+ groupedSeries: groups,
390
+ };
391
+ }, [series, getColor]);
392
+
393
+ // Format time for display the tick in the x axis
394
+ const formatTime = useMemo(
395
+ () => (timestamp: number) => {
396
+ const date = new Date(timestamp);
397
+ return DAY_MONTH_ABBREVIATED_HOUR_MINUTE.format(date).replace(',', '');
398
+ },
399
+ [],
400
+ );
401
+
402
+ return (
403
+ <LineTemporalChartWrapper>
404
+ <ChartHeader>
405
+ <ChartTitleText>
406
+ {title} {unitLabel && `(${unitLabel})`}
407
+ </ChartTitleText>
408
+ {helpText && (
409
+ <TooltipComponent
410
+ placement={'right'}
411
+ overlay={<SmallerText>{helpText}</SmallerText>}
412
+ >
413
+ <Icon name="Info" color={theme.buttonSecondary} />
414
+ </TooltipComponent>
415
+ )}
416
+ {isLoading && <Loader />}
417
+ </ChartHeader>
418
+ <ResponsiveContainer width="100%" height={height}>
419
+ <LineChart
420
+ data={rechartsData}
421
+ ref={chartRef}
422
+ margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
423
+ aria-label={`Time series chart for ${title}`}
424
+ >
425
+ <CartesianGrid
426
+ vertical={true}
427
+ horizontal={true}
428
+ verticalPoints={[0]}
429
+ horizontalPoints={[0]}
430
+ stroke={theme.border}
431
+ fill={theme.backgroundLevel4}
432
+ strokeWidth={1}
433
+ />
434
+ <XAxis
435
+ dataKey="timestamp"
436
+ type="number"
437
+ domain={['dataMin', 'dataMax']}
438
+ ticks={xAxisTicks}
439
+ tickFormatter={formatTime}
440
+ tickCount={5}
441
+ tick={{
442
+ fill: theme.textSecondary,
443
+ fontSize: fontSize.smaller,
444
+ }}
445
+ axisLine={{ stroke: theme.border }}
446
+ />
447
+ <YAxis
448
+ orientation="right"
449
+ allowDataOverflow={false}
450
+ label={{
451
+ value: yAxisTitle,
452
+ angle: 90,
453
+ position: 'insideRight',
454
+ style: {
455
+ textAnchor: 'middle',
456
+ fill: theme.textSecondary,
457
+ fontSize: fontSize.smaller,
458
+ },
459
+ }}
460
+ domain={
461
+ yAxisType === 'percentage'
462
+ ? [0, 100]
463
+ : yAxisType === 'symmetrical'
464
+ ? [-topValue, topValue]
465
+ : [0, topValue]
466
+ }
467
+ axisLine={{ stroke: theme.border }}
468
+ tick={{
469
+ fill: theme.textSecondary,
470
+ fontSize: fontSize.smaller,
471
+ }}
472
+ tickFormatter={(value) => Math.round(value).toString()}
473
+ />
474
+ <Tooltip content={<CustomTooltip unitLabel={unitLabel} />} />
475
+ {/* Add horizontal line at y=0 for symmetrical charts */}
476
+ {yAxisType === 'symmetrical' && (
477
+ <ReferenceLine y={0} stroke={theme.border} />
478
+ )}
479
+
480
+ {/* Chart lines */}
481
+ {Object.entries(groupedSeries).map(([resource, resourceSeries]) =>
482
+ resourceSeries.map((serie, serieIndex) => {
483
+ const label = serie.getTooltipLabel(
484
+ serie.metricPrefix,
485
+ serie.resource,
486
+ );
487
+ return (
488
+ <Line
489
+ key={`${title}-${resource}-${serieIndex}`}
490
+ type="monotone"
491
+ dataKey={label}
492
+ stroke={colorMapping[resource]}
493
+ dot={false}
494
+ />
495
+ );
496
+ }),
497
+ )}
498
+ </LineChart>
499
+ </ResponsiveContainer>
500
+ </LineTemporalChartWrapper>
501
+ );
502
+ }
@@ -1,16 +1,11 @@
1
1
  import React from 'react';
2
2
  import { render, screen, waitFor } from '@testing-library/react';
3
3
  import { SearchInput, Props } from './SearchInput.component';
4
- import { QueryClient, QueryClientProvider } from 'react-query';
5
4
  import userEvent from '@testing-library/user-event';
6
5
 
7
- const queryClient = new QueryClient();
8
-
9
6
  const SearchInputRender = (props: Props) => {
10
7
  return (
11
- <QueryClientProvider client={queryClient}>
12
- <SearchInput {...props} />
13
- </QueryClientProvider>
8
+ <SearchInput {...props} />
14
9
  );
15
10
  };
16
11
 
@@ -19,8 +14,9 @@ describe('SearchInput', () => {
19
14
  searchInput: () => screen.getByRole('searchbox'),
20
15
  clearButton: () => screen.queryByRole('button'),
21
16
  };
22
- it('should render the SearchInput component', () => {
17
+ it('should render the SearchInput component', async () => {
23
18
  render(<SearchInputRender value="" onChange={() => {}} />);
19
+ await waitFor(() => screen.queryAllByRole('img', { hidden: true }));
24
20
 
25
21
  const searchInput = selectors.searchInput();
26
22
  expect(searchInput).toBeInTheDocument();
@@ -10,6 +10,8 @@ import {
10
10
  useImperativeHandle,
11
11
  ReactNode,
12
12
  Ref,
13
+ useMemo,
14
+ useCallback,
13
15
  } from 'react';
14
16
  import { ScrollbarWrapper, Tooltip } from '../../index';
15
17
  import {
@@ -562,22 +564,28 @@ const SelectWithOptionContext = forwardRef<
562
564
  >((props, ref) => {
563
565
  const [options, setOptions] = useState<Record<string, SelectOptionProps>>({});
564
566
 
565
- const register = (option: SelectOptionProps) => {
567
+ const register = useCallback((option: SelectOptionProps) => {
566
568
  setOptions((prevOptions) => ({
567
569
  ...prevOptions,
568
570
  [option.value]: option,
569
571
  }));
570
- };
572
+ }, []);
571
573
 
572
- const unregister = (value: string) => {
574
+ const unregister = useCallback((value: string) => {
573
575
  setOptions((prevOptions) => {
574
576
  const { [value]: _, ...rest } = prevOptions;
575
577
  return rest;
576
578
  });
577
- };
579
+ }, []);
580
+
581
+ const contextValue = useMemo(() => ({
582
+ options,
583
+ register,
584
+ unregister
585
+ }), [options, register, unregister]);
578
586
 
579
587
  return (
580
- <OptionContext.Provider value={{ options, register, unregister }}>
588
+ <OptionContext.Provider value={contextValue}>
581
589
  <>
582
590
  <SelectBox {...props} selectRef={ref} />
583
591
  {props.children}