@hero-design/rn 8.114.0 → 8.115.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @hero-design/rn
2
2
 
3
+ ## 8.115.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#4620](https://github.com/Thinkei/hero-design/pull/4620) [`305a264890d57ea01402684d2ba42e34908cd852`](https://github.com/Thinkei/hero-design/commit/305a264890d57ea01402684d2ba42e34908cd852) Thanks [@vinhphan-eh](https://github.com/vinhphan-eh)! - [Progress.SegmentedBar] Add custom color config and empty state
8
+
3
9
  ## 8.114.0
4
10
 
5
11
  ### Minor Changes
package/lib/index.js CHANGED
@@ -25402,11 +25402,13 @@ var ProgressStep = function ProgressStep(_ref) {
25402
25402
  }));
25403
25403
  };
25404
25404
 
25405
+ var SEGMENTED_BAR_COLORS = ['primaryMedium', 'blueMedium', 'greenMedium', 'redMedium', 'orangeMedium', 'yellowMedium', 'pinkMedium', 'greyMedium', 'primaryLight', 'blueLight', 'greenLight', 'redLight', 'orangeLight', 'yellowLight', 'pinkLight', 'greyLight', 'success', 'onSuccessSurface', 'onInfoSurface', 'warning', 'onWarningSurface', 'error', 'onErrorSurface', 'archive', 'onArchiveSurface'];
25405
25406
  var MIN_PROGRESS_VALUE = 0;
25406
25407
  var MAX_PROGRESS_VALUE = 100;
25407
25408
  var SEGMENTED_BAR_ERRORS = {
25408
25409
  negativeValue: '[Progress.SegmentedBar] value:$value is less than 0',
25409
- legendDisplayValues: '[Progress.SegmentedBar] legendConfig.displayValues must include a non-null/non-undefined entry for each data series (length must match and no null/undefined values are allowed)'
25410
+ legendDisplayValues: '[Progress.SegmentedBar] legendConfig.displayValues must include a non-null/non-undefined entry for each data series (length must match and no null/undefined values are allowed)',
25411
+ customColorsInvalid: '[Progress.SegmentedBar] Invalid styleConfig.series: length must match the number of data series, each entry must include a color value, and labels must match the data series labels'
25410
25412
  };
25411
25413
 
25412
25414
  var useValidateSegmentedBarData = function useValidateSegmentedBarData(_ref) {
@@ -25454,6 +25456,115 @@ var useValidateSegmentedBarLegendDisplayValues = function useValidateSegmentedBa
25454
25456
  assert(!(hasLengthMismatch || hasEmptyEntry), SEGMENTED_BAR_ERRORS.legendDisplayValues);
25455
25457
  return valuesToUse;
25456
25458
  };
25459
+ var useValidateSegmentedBarStyleConfig = function useValidateSegmentedBarStyleConfig(_ref3) {
25460
+ var dataSeries = _ref3.dataSeries,
25461
+ customSeries = _ref3.customSeries;
25462
+ var _React$useMemo3 = React__namespace.default.useMemo(function () {
25463
+ if (!customSeries || dataSeries.length === 0) {
25464
+ return {
25465
+ hasInvalidLength: false,
25466
+ hasMissingColor: false,
25467
+ hasMissingLabel: false
25468
+ };
25469
+ }
25470
+ var invalidLength = !Array.isArray(customSeries) || customSeries.length !== dataSeries.length;
25471
+ if (invalidLength) {
25472
+ return {
25473
+ hasInvalidLength: invalidLength,
25474
+ hasMissingColor: false,
25475
+ hasMissingLabel: false
25476
+ };
25477
+ }
25478
+ var missingColor = customSeries.some(function (item) {
25479
+ return !(item !== null && item !== void 0 && item.color);
25480
+ });
25481
+ var providedLabels = new Set(customSeries.map(function (item) {
25482
+ return item.label;
25483
+ }));
25484
+ var missingLabel = dataSeries.some(function (_ref4) {
25485
+ var label = _ref4.label;
25486
+ return !providedLabels.has(label);
25487
+ });
25488
+ return {
25489
+ hasInvalidLength: invalidLength,
25490
+ hasMissingColor: missingColor,
25491
+ hasMissingLabel: missingLabel
25492
+ };
25493
+ }, [customSeries, dataSeries]),
25494
+ hasInvalidLength = _React$useMemo3.hasInvalidLength,
25495
+ hasMissingColor = _React$useMemo3.hasMissingColor,
25496
+ hasMissingLabel = _React$useMemo3.hasMissingLabel;
25497
+ assert(!(hasInvalidLength || hasMissingColor || hasMissingLabel), SEGMENTED_BAR_ERRORS.customColorsInvalid);
25498
+ };
25499
+
25500
+ /**
25501
+ * Retrieves a color value from a theme source by token.
25502
+ */
25503
+ var getThemeColorValue = function getThemeColorValue(source, token) {
25504
+ var value = source === null || source === void 0 ? void 0 : source[token];
25505
+ return typeof value === 'string' ? value : undefined;
25506
+ };
25507
+ /**
25508
+ * Resolves a SegmentedBar color token to a concrete CSS color string.
25509
+ * Falls back to the raw token string if not found.
25510
+ */
25511
+ var getColorFromList = function getColorFromList(_ref) {
25512
+ var _ref2, _getThemeColorValue;
25513
+ var theme = _ref.theme,
25514
+ token = _ref.token;
25515
+ return (_ref2 = (_getThemeColorValue = getThemeColorValue(theme.colors, token)) !== null && _getThemeColorValue !== void 0 ? _getThemeColorValue : getThemeColorValue(palette$9, token)) !== null && _ref2 !== void 0 ? _ref2 : token;
25516
+ };
25517
+ /**
25518
+ * Maps custom series colors by matching labels.
25519
+ */
25520
+ var mapCustomSeriesColors = function mapCustomSeriesColors(_ref3) {
25521
+ var dataSeries = _ref3.dataSeries,
25522
+ customSeries = _ref3.customSeries,
25523
+ theme = _ref3.theme;
25524
+ var colorByLabel = new Map(customSeries.map(function (_ref4) {
25525
+ var label = _ref4.label,
25526
+ color = _ref4.color;
25527
+ return [label, getColorFromList({
25528
+ theme: theme,
25529
+ token: color
25530
+ })];
25531
+ }));
25532
+ return dataSeries.map(function (_ref5) {
25533
+ var label = _ref5.label;
25534
+ var resolvedColor = colorByLabel.get(label);
25535
+ return resolvedColor || '';
25536
+ });
25537
+ };
25538
+ /**
25539
+ * Returns an array of colors (index-aligned to the segmented bar data).
25540
+ *
25541
+ * - if `customSeries` is provided, uses the provided series colors (resolved via theme/palette)
25542
+ * - otherwise returns the default palette-based colors
25543
+ */
25544
+ var useSegmentedBarColors = function useSegmentedBarColors(_ref6) {
25545
+ var dataSeries = _ref6.dataSeries,
25546
+ customSeries = _ref6.customSeries;
25547
+ var theme = useTheme$1();
25548
+ var colors = React.useMemo(function () {
25549
+ if (customSeries !== undefined) {
25550
+ return mapCustomSeriesColors({
25551
+ dataSeries: dataSeries,
25552
+ customSeries: customSeries,
25553
+ theme: theme
25554
+ });
25555
+ }
25556
+ // Default colors
25557
+ return Array.from({
25558
+ length: dataSeries.length
25559
+ }, function (_, index) {
25560
+ return getColorFromList({
25561
+ theme: theme,
25562
+ token: SEGMENTED_BAR_COLORS[index % SEGMENTED_BAR_COLORS.length]
25563
+ });
25564
+ });
25565
+ }, [customSeries, dataSeries, theme]);
25566
+ return colors;
25567
+ };
25457
25568
 
25458
25569
  var LegendBox = index$c(Box)(function (_ref) {
25459
25570
  var theme = _ref.theme;
@@ -25546,14 +25657,23 @@ var StyledSegment = index$c(Box)(function (_ref3) {
25546
25657
  width: "".concat(themeWidthPercent, "%")
25547
25658
  };
25548
25659
  });
25660
+ var StyledEmptyText = index$c(Typography.Caption)(function (_ref4) {
25661
+ var theme = _ref4.theme;
25662
+ return {
25663
+ marginTop: theme.__hd__.progress.space.segmentedLegendMarginTop
25664
+ };
25665
+ });
25549
25666
 
25550
- var _excluded$j = ["data", "headerConfig", "legendConfig", "style", "testID"];
25667
+ var _excluded$j = ["data", "headerConfig", "emptyText", "legendConfig", "styleConfig", "style", "testID", "collapsable"];
25551
25668
  var ProgressSegmentedBar = function ProgressSegmentedBar(_ref) {
25552
25669
  var data = _ref.data,
25553
25670
  headerConfig = _ref.headerConfig,
25671
+ emptyText = _ref.emptyText,
25554
25672
  legendConfig = _ref.legendConfig,
25673
+ styleConfig = _ref.styleConfig,
25555
25674
  style = _ref.style,
25556
25675
  testID = _ref.testID,
25676
+ collapsable = _ref.collapsable,
25557
25677
  nativeProps = _objectWithoutProperties(_ref, _excluded$j);
25558
25678
  var values = React__namespace.default.useMemo(function () {
25559
25679
  return data.map(function (s) {
@@ -25569,6 +25689,10 @@ var ProgressSegmentedBar = function ProgressSegmentedBar(_ref) {
25569
25689
  legendConfig: legendConfig,
25570
25690
  fallbackValues: values
25571
25691
  });
25692
+ useValidateSegmentedBarStyleConfig({
25693
+ dataSeries: data,
25694
+ customSeries: styleConfig === null || styleConfig === void 0 ? void 0 : styleConfig.series
25695
+ });
25572
25696
  var theme = useTheme$1();
25573
25697
  var totalPercent = React__namespace.default.useMemo(function () {
25574
25698
  return values.reduce(function (acc, value) {
@@ -25579,8 +25703,10 @@ var ProgressSegmentedBar = function ProgressSegmentedBar(_ref) {
25579
25703
  var remainderPercent = React__namespace.default.useMemo(function () {
25580
25704
  return totalPercent >= MAX_PROGRESS_VALUE ? 0 : MAX_PROGRESS_VALUE - totalPercent;
25581
25705
  }, [totalPercent]);
25582
- // Mock colors
25583
- var segmentColors = [theme.colors.primary, theme.colors.success, theme.colors.warning, theme.colors.error, theme.colors.info, theme.colors.archived];
25706
+ var segmentColors = useSegmentedBarColors({
25707
+ dataSeries: data,
25708
+ customSeries: styleConfig === null || styleConfig === void 0 ? void 0 : styleConfig.series
25709
+ });
25584
25710
  var headerRight = React__namespace.default.useMemo(function () {
25585
25711
  return getTextComponent(headerConfig === null || headerConfig === void 0 ? void 0 : headerConfig.right, /*#__PURE__*/React__namespace.default.createElement(Typography.Caption, {
25586
25712
  intent: "muted"
@@ -25588,9 +25714,11 @@ var ProgressSegmentedBar = function ProgressSegmentedBar(_ref) {
25588
25714
  }, [headerConfig === null || headerConfig === void 0 ? void 0 : headerConfig.right]);
25589
25715
  var showHeader = Boolean((headerConfig === null || headerConfig === void 0 ? void 0 : headerConfig.left) || headerRight);
25590
25716
  var showLegend = Boolean(legendConfig && Object.keys(legendConfig).length > 0);
25717
+ var showEmptyText = Boolean(!data.length && emptyText);
25591
25718
  return /*#__PURE__*/React__namespace.default.createElement(Box, _extends$1({}, nativeProps, {
25592
25719
  style: style,
25593
- testID: testID
25720
+ testID: testID,
25721
+ collapsable: collapsable || false
25594
25722
  }), showHeader ? /*#__PURE__*/React__namespace.default.createElement(StyledHeader, {
25595
25723
  testID: testID ? "".concat(testID, "-header") : undefined
25596
25724
  }, headerConfig !== null && headerConfig !== void 0 && headerConfig.left ? /*#__PURE__*/React__namespace.default.createElement(Typography.Body, {
@@ -25619,8 +25747,11 @@ var ProgressSegmentedBar = function ProgressSegmentedBar(_ref) {
25619
25747
  themeColor: theme.__hd__.progress.colors.segmentedRemainderBackground,
25620
25748
  themeWidthPercent: remainderPercent,
25621
25749
  testID: testID ? "".concat(testID, "-segment-remainder") : undefined
25622
- }) : null), showLegend ? /*#__PURE__*/React__namespace.default.createElement(LegendBox, {
25623
- testID: testID ? "".concat(testID, "-legend") : 'segmented-progress-legend'
25750
+ }) : null), showEmptyText ? /*#__PURE__*/React__namespace.default.createElement(StyledEmptyText, {
25751
+ intent: "muted",
25752
+ testID: testID ? "".concat(testID, "-empty-text") : undefined
25753
+ }, emptyText) : null, showLegend ? /*#__PURE__*/React__namespace.default.createElement(LegendBox, {
25754
+ testID: testID ? "".concat(testID, "-legend") : undefined
25624
25755
  }, data.map(function (item, index) {
25625
25756
  return /*#__PURE__*/React__namespace.default.createElement(ProgressSegmentedBarLegendItem, {
25626
25757
  key: "".concat(item.label, "-").concat(segmentColors[index % segmentColors.length]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hero-design/rn",
3
- "version": "8.114.0",
3
+ "version": "8.115.0",
4
4
  "license": "MIT",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -1,4 +1,5 @@
1
1
  import styled from '@emotion/native';
2
+ import Typography from '../../Typography';
2
3
  import Box from '../../Box';
3
4
 
4
5
  const StyledHeader = styled(Box)(({ theme }) => ({
@@ -27,4 +28,8 @@ const StyledSegment = styled(Box)<{
27
28
  width: `${themeWidthPercent}%`,
28
29
  }));
29
30
 
30
- export { StyledHeader, StyledSegment, StyledTrack };
31
+ const StyledEmptyText = styled(Typography.Caption)(({ theme }) => ({
32
+ marginTop: theme.__hd__.progress.space.segmentedLegendMarginTop,
33
+ }));
34
+
35
+ export { StyledHeader, StyledSegment, StyledTrack, StyledEmptyText };
@@ -1,4 +1,6 @@
1
+ import { mobileVisualisationPalette } from '@hero-design/colors';
1
2
  import React from 'react';
3
+ import { StyleSheet } from 'react-native';
2
4
  import renderWithTheme from '../../../../testHelpers/renderWithTheme';
3
5
  import ProgressSegmentedBar from '..';
4
6
  import { SEGMENTED_BAR_ERRORS } from '../constants';
@@ -77,6 +79,33 @@ describe('Progress.SegmentedBar', () => {
77
79
  expect(queryByTestId('progress-segmented-legend')).toBeNull();
78
80
  });
79
81
 
82
+ it('renders empty text when data is empty', () => {
83
+ const { getByTestId, getByText, queryByTestId } = renderWithTheme(
84
+ <ProgressSegmentedBar
85
+ testID="progress-segmented"
86
+ emptyText="No data yet"
87
+ data={[]}
88
+ />
89
+ );
90
+
91
+ expect(getByTestId('progress-segmented-track')).toBeVisible();
92
+ expect(getByTestId('progress-segmented-empty-text')).toBeVisible();
93
+ expect(getByText('No data yet')).toBeVisible();
94
+ expect(queryByTestId('progress-segmented-legend')).toBeNull();
95
+ });
96
+
97
+ it('does not render empty text when data exists', () => {
98
+ const { queryByTestId } = renderWithTheme(
99
+ <ProgressSegmentedBar
100
+ testID="progress-segmented"
101
+ emptyText="No data yet"
102
+ data={[{ label: 'A', data: 10 }]}
103
+ />
104
+ );
105
+
106
+ expect(queryByTestId('progress-segmented-empty-text')).toBeNull();
107
+ });
108
+
80
109
  it('throws on negative segment value', () => {
81
110
  expect(() =>
82
111
  renderWithTheme(
@@ -112,4 +141,83 @@ describe('Progress.SegmentedBar', () => {
112
141
  ).toThrow(SEGMENTED_BAR_ERRORS.legendDisplayValues);
113
142
  }
114
143
  );
144
+
145
+ it('uses custom colors from styleConfig', () => {
146
+ const { getByTestId } = renderWithTheme(
147
+ <ProgressSegmentedBar
148
+ testID="progress-segmented"
149
+ styleConfig={{
150
+ series: [
151
+ { label: 'A', color: 'primaryLight' },
152
+ { label: 'B', color: 'blueLight' },
153
+ ],
154
+ }}
155
+ data={[
156
+ { label: 'A', data: 40 },
157
+ { label: 'B', data: 60 },
158
+ ]}
159
+ legendConfig={{ displayValues: ['40%', '60%'] }}
160
+ />
161
+ );
162
+
163
+ const firstSegmentStyle = StyleSheet.flatten(
164
+ getByTestId('progress-segmented-segment-A').props.style
165
+ );
166
+ const secondSegmentStyle = StyleSheet.flatten(
167
+ getByTestId('progress-segmented-segment-B').props.style
168
+ );
169
+
170
+ expect(firstSegmentStyle?.backgroundColor).toBe(
171
+ mobileVisualisationPalette.primaryLight
172
+ );
173
+ expect(secondSegmentStyle?.backgroundColor).toBe(
174
+ mobileVisualisationPalette.blueLight
175
+ );
176
+ });
177
+
178
+ it('uses default colors when styleConfig is not provided', () => {
179
+ const { getByTestId } = renderWithTheme(
180
+ <ProgressSegmentedBar
181
+ testID="progress-segmented"
182
+ data={[
183
+ { label: 'A', data: 40 },
184
+ { label: 'B', data: 60 },
185
+ ]}
186
+ />
187
+ );
188
+
189
+ const firstSegmentStyle = StyleSheet.flatten(
190
+ getByTestId('progress-segmented-segment-A').props.style
191
+ );
192
+ const secondSegmentStyle = StyleSheet.flatten(
193
+ getByTestId('progress-segmented-segment-B').props.style
194
+ );
195
+
196
+ expect(firstSegmentStyle?.backgroundColor).toBe(
197
+ mobileVisualisationPalette.primaryMedium
198
+ );
199
+ expect(secondSegmentStyle?.backgroundColor).toBe(
200
+ mobileVisualisationPalette.blueMedium
201
+ );
202
+ });
203
+
204
+ it.each`
205
+ description | styleConfig
206
+ ${'length mismatches data'} | ${{ series: [{ label: 'A', color: 'primaryLight' }] }}
207
+ ${'labels do not match data labels'} | ${{ series: [{ label: 'C', color: 'primaryLight' }, { label: 'D', color: 'blueLight' }] }}
208
+ ${'colors are missing'} | ${{ series: [{ label: 'A', color: '' as any }, { label: 'B', color: undefined as any }] }}
209
+ `('throws when styleConfig is invalid: $description', ({ styleConfig }) => {
210
+ expect(() =>
211
+ renderWithTheme(
212
+ <ProgressSegmentedBar
213
+ testID="progress-segmented"
214
+ styleConfig={styleConfig}
215
+ data={[
216
+ { label: 'A', data: 40 },
217
+ { label: 'B', data: 60 },
218
+ ]}
219
+ />
220
+ )
221
+ ).toThrow(SEGMENTED_BAR_ERRORS.customColorsInvalid);
222
+ });
115
223
  });
@@ -1,3 +1,33 @@
1
+ export const SEGMENTED_BAR_COLORS = [
2
+ 'primaryMedium',
3
+ 'blueMedium',
4
+ 'greenMedium',
5
+ 'redMedium',
6
+ 'orangeMedium',
7
+ 'yellowMedium',
8
+ 'pinkMedium',
9
+ 'greyMedium',
10
+ 'primaryLight',
11
+ 'blueLight',
12
+ 'greenLight',
13
+ 'redLight',
14
+ 'orangeLight',
15
+ 'yellowLight',
16
+ 'pinkLight',
17
+ 'greyLight',
18
+ 'success',
19
+ 'onSuccessSurface',
20
+ 'onInfoSurface',
21
+ 'warning',
22
+ 'onWarningSurface',
23
+ 'error',
24
+ 'onErrorSurface',
25
+ 'archive',
26
+ 'onArchiveSurface',
27
+ ] as const;
28
+
29
+ export type SegmentedBarColorToken = typeof SEGMENTED_BAR_COLORS[number];
30
+
1
31
  export const MIN_PROGRESS_VALUE = 0;
2
32
  export const MAX_PROGRESS_VALUE = 100;
3
33
 
@@ -5,4 +35,6 @@ export const SEGMENTED_BAR_ERRORS = {
5
35
  negativeValue: '[Progress.SegmentedBar] value:$value is less than 0',
6
36
  legendDisplayValues:
7
37
  '[Progress.SegmentedBar] legendConfig.displayValues must include a non-null/non-undefined entry for each data series (length must match and no null/undefined values are allowed)',
38
+ customColorsInvalid:
39
+ '[Progress.SegmentedBar] Invalid styleConfig.series: length must match the number of data series, each entry must include a color value, and labels must match the data series labels',
8
40
  } as const;
@@ -0,0 +1,102 @@
1
+ import { useTheme } from '@emotion/react';
2
+ import { useMemo } from 'react';
3
+ import { mobileVisualisationPalette } from '@hero-design/colors';
4
+ import type { Theme } from '../../../../theme';
5
+ import type { SegmentedBarData, SegmentedBarStyleConfig } from '../types';
6
+ import {
7
+ SEGMENTED_BAR_COLORS,
8
+ type SegmentedBarColorToken,
9
+ } from '../constants';
10
+
11
+ /**
12
+ * Retrieves a color value from a theme source by token.
13
+ */
14
+ const getThemeColorValue = (
15
+ source: Record<string, unknown> | undefined,
16
+ token: SegmentedBarColorToken
17
+ ): string | undefined => {
18
+ const value = source?.[token];
19
+ return typeof value === 'string' ? value : undefined;
20
+ };
21
+
22
+ /**
23
+ * Resolves a SegmentedBar color token to a concrete CSS color string.
24
+ * Falls back to the raw token string if not found.
25
+ */
26
+ const getColorFromList = ({
27
+ theme,
28
+ token,
29
+ }: {
30
+ theme: Theme;
31
+ token: SegmentedBarColorToken;
32
+ }): string => {
33
+ return (
34
+ getThemeColorValue(theme.colors, token) ??
35
+ getThemeColorValue(mobileVisualisationPalette, token) ??
36
+ token
37
+ );
38
+ };
39
+
40
+ /**
41
+ * Maps custom series colors by matching labels.
42
+ */
43
+ const mapCustomSeriesColors = ({
44
+ dataSeries,
45
+ customSeries,
46
+ theme,
47
+ }: {
48
+ dataSeries: SegmentedBarData;
49
+ customSeries: NonNullable<SegmentedBarStyleConfig['series']>;
50
+ theme: Theme;
51
+ }): string[] => {
52
+ const colorByLabel = new Map(
53
+ customSeries.map(({ label, color }) => [
54
+ label,
55
+ getColorFromList({
56
+ theme,
57
+ token: color,
58
+ }),
59
+ ])
60
+ );
61
+
62
+ return dataSeries.map(({ label }) => {
63
+ const resolvedColor = colorByLabel.get(label);
64
+ return resolvedColor || '';
65
+ });
66
+ };
67
+
68
+ /**
69
+ * Returns an array of colors (index-aligned to the segmented bar data).
70
+ *
71
+ * - if `customSeries` is provided, uses the provided series colors (resolved via theme/palette)
72
+ * - otherwise returns the default palette-based colors
73
+ */
74
+ export const useSegmentedBarColors = ({
75
+ dataSeries,
76
+ customSeries,
77
+ }: {
78
+ dataSeries: SegmentedBarData;
79
+ customSeries?: SegmentedBarStyleConfig['series'];
80
+ }): string[] => {
81
+ const theme = useTheme();
82
+
83
+ const colors = useMemo(() => {
84
+ if (customSeries !== undefined) {
85
+ return mapCustomSeriesColors({
86
+ dataSeries,
87
+ customSeries,
88
+ theme,
89
+ });
90
+ }
91
+
92
+ // Default colors
93
+ return Array.from({ length: dataSeries.length }, (_, index) =>
94
+ getColorFromList({
95
+ theme,
96
+ token: SEGMENTED_BAR_COLORS[index % SEGMENTED_BAR_COLORS.length],
97
+ })
98
+ );
99
+ }, [customSeries, dataSeries, theme]);
100
+
101
+ return colors;
102
+ };
@@ -6,6 +6,8 @@ import { assert } from '../../../../utils/helpers';
6
6
  import type {
7
7
  SegmentedBarLegendConfig,
8
8
  SegmentedBarLegendDisplayValue,
9
+ SegmentedBarData,
10
+ SegmentedBarStyleConfig,
9
11
  } from '../types';
10
12
 
11
13
  export const useValidateSegmentedBarData = ({
@@ -69,3 +71,51 @@ export const useValidateSegmentedBarLegendDisplayValues = ({
69
71
  );
70
72
  return valuesToUse;
71
73
  };
74
+
75
+ export const useValidateSegmentedBarStyleConfig = ({
76
+ dataSeries,
77
+ customSeries,
78
+ }: {
79
+ dataSeries: SegmentedBarData;
80
+ customSeries: SegmentedBarStyleConfig['series'];
81
+ }): void => {
82
+ const { hasInvalidLength, hasMissingColor, hasMissingLabel } =
83
+ React.useMemo(() => {
84
+ if (!customSeries || dataSeries.length === 0) {
85
+ return {
86
+ hasInvalidLength: false,
87
+ hasMissingColor: false,
88
+ hasMissingLabel: false,
89
+ };
90
+ }
91
+
92
+ const invalidLength =
93
+ !Array.isArray(customSeries) ||
94
+ customSeries.length !== dataSeries.length;
95
+
96
+ if (invalidLength) {
97
+ return {
98
+ hasInvalidLength: invalidLength,
99
+ hasMissingColor: false,
100
+ hasMissingLabel: false,
101
+ };
102
+ }
103
+
104
+ const missingColor = customSeries.some((item) => !item?.color);
105
+ const providedLabels = new Set(customSeries.map((item) => item.label));
106
+ const missingLabel = dataSeries.some(
107
+ ({ label }) => !providedLabels.has(label)
108
+ );
109
+
110
+ return {
111
+ hasInvalidLength: invalidLength,
112
+ hasMissingColor: missingColor,
113
+ hasMissingLabel: missingLabel,
114
+ };
115
+ }, [customSeries, dataSeries]);
116
+
117
+ assert(
118
+ !(hasInvalidLength || hasMissingColor || hasMissingLabel),
119
+ SEGMENTED_BAR_ERRORS.customColorsInvalid
120
+ );
121
+ };
@@ -9,19 +9,29 @@ import { MAX_PROGRESS_VALUE, MIN_PROGRESS_VALUE } from './constants';
9
9
  import {
10
10
  useValidateSegmentedBarData,
11
11
  useValidateSegmentedBarLegendDisplayValues,
12
+ useValidateSegmentedBarStyleConfig,
12
13
  } from './hooks/validation';
14
+ import { useSegmentedBarColors } from './hooks/colors';
13
15
  import LegendItem from './Legend';
14
16
  import { LegendBox } from './StyledLegend';
15
- import { StyledHeader, StyledSegment, StyledTrack } from './StyledSegmentedBar';
17
+ import {
18
+ StyledEmptyText,
19
+ StyledHeader,
20
+ StyledSegment,
21
+ StyledTrack,
22
+ } from './StyledSegmentedBar';
16
23
  import type { ProgressSegmentedBarProps } from './types';
17
24
  import { getTextComponent } from './utils';
18
25
 
19
26
  const ProgressSegmentedBar = ({
20
27
  data,
21
28
  headerConfig,
29
+ emptyText,
22
30
  legendConfig,
31
+ styleConfig,
23
32
  style,
24
33
  testID,
34
+ collapsable,
25
35
  ...nativeProps
26
36
  }: ProgressSegmentedBarProps): ReactElement => {
27
37
  const values = React.useMemo(() => data.map((s) => s.data), [data]);
@@ -33,6 +43,11 @@ const ProgressSegmentedBar = ({
33
43
  fallbackValues: values,
34
44
  });
35
45
 
46
+ useValidateSegmentedBarStyleConfig({
47
+ dataSeries: data,
48
+ customSeries: styleConfig?.series,
49
+ });
50
+
36
51
  const theme = useTheme();
37
52
  const totalPercent = React.useMemo(
38
53
  () => values.reduce((acc, value) => acc + value, 0),
@@ -47,16 +62,10 @@ const ProgressSegmentedBar = ({
47
62
  ? 0
48
63
  : MAX_PROGRESS_VALUE - totalPercent;
49
64
  }, [totalPercent]);
50
-
51
- // Mock colors
52
- const segmentColors = [
53
- theme.colors.primary,
54
- theme.colors.success,
55
- theme.colors.warning,
56
- theme.colors.error,
57
- theme.colors.info,
58
- theme.colors.archived,
59
- ];
65
+ const segmentColors = useSegmentedBarColors({
66
+ dataSeries: data,
67
+ customSeries: styleConfig?.series,
68
+ });
60
69
 
61
70
  const headerRight = React.useMemo(
62
71
  () =>
@@ -73,9 +82,15 @@ const ProgressSegmentedBar = ({
73
82
  const showLegend = Boolean(
74
83
  legendConfig && Object.keys(legendConfig).length > 0
75
84
  );
85
+ const showEmptyText = Boolean(!data.length && emptyText);
76
86
 
77
87
  return (
78
- <Box {...nativeProps} style={style} testID={testID}>
88
+ <Box
89
+ {...nativeProps}
90
+ style={style}
91
+ testID={testID}
92
+ collapsable={collapsable || false}
93
+ >
79
94
  {showHeader ? (
80
95
  <StyledHeader testID={testID ? `${testID}-header` : undefined}>
81
96
  {headerConfig?.left ? (
@@ -129,10 +144,17 @@ const ProgressSegmentedBar = ({
129
144
  ) : null}
130
145
  </StyledTrack>
131
146
 
132
- {showLegend ? (
133
- <LegendBox
134
- testID={testID ? `${testID}-legend` : 'segmented-progress-legend'}
147
+ {showEmptyText ? (
148
+ <StyledEmptyText
149
+ intent="muted"
150
+ testID={testID ? `${testID}-empty-text` : undefined}
135
151
  >
152
+ {emptyText}
153
+ </StyledEmptyText>
154
+ ) : null}
155
+
156
+ {showLegend ? (
157
+ <LegendBox testID={testID ? `${testID}-legend` : undefined}>
136
158
  {data.map((item, index) => (
137
159
  <LegendItem
138
160
  key={`${item.label}-${
@@ -1,6 +1,7 @@
1
1
  import type { ReactElement, ReactNode } from 'react';
2
2
 
3
3
  import type { ViewProps } from 'react-native';
4
+ import type { SegmentedBarColorToken } from './constants';
4
5
 
5
6
  export interface SegmentedBarSeries<Data> {
6
7
  /**
@@ -15,6 +16,18 @@ export interface SegmentedBarSeries<Data> {
15
16
 
16
17
  export type SegmentedBarData = Array<SegmentedBarSeries<number>>;
17
18
 
19
+ export interface SegmentedBarStyleConfig {
20
+ /**
21
+ * Colors config per series.
22
+ * Provide one entry per series in `data` matched by the `label` value (order-agnostic).
23
+ * When provided, number of entries must match `data.length` and all data labels must have a corresponding entry, otherwise an error will be thrown.
24
+ */
25
+ series?: Array<{
26
+ label: string;
27
+ color: SegmentedBarColorToken;
28
+ }>;
29
+ }
30
+
18
31
  export interface SegmentedBarHeaderConfig {
19
32
  /**
20
33
  * Left header content.
@@ -54,9 +67,19 @@ export interface ProgressSegmentedBarProps extends ViewProps {
54
67
  */
55
68
  headerConfig?: SegmentedBarHeaderConfig;
56
69
 
70
+ /**
71
+ * Optional text shown when there is no data.
72
+ */
73
+ emptyText?: string;
74
+
57
75
  /**
58
76
  * Optional legend config.
59
77
  * If provided, the legend is rendered below the segmented bar.
60
78
  */
61
79
  legendConfig?: SegmentedBarLegendConfig;
80
+
81
+ /**
82
+ * Custom style config to override series colors.
83
+ */
84
+ styleConfig?: SegmentedBarStyleConfig;
62
85
  }