@hero-design/rn 8.99.4 → 8.100.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/.turbo/turbo-build.log +8 -3
- package/CHANGELOG.md +17 -0
- package/es/index.js +5621 -690
- package/jest.config.js +1 -1
- package/lib/index.js +5545 -613
- package/package.json +4 -2
- package/src/components/Avatar/AvatarStack/utils.ts +6 -4
- package/src/components/Badge/Status.tsx +1 -1
- package/src/components/Badge/__tests__/Status.spec.tsx +20 -0
- package/src/components/Badge/__tests__/__snapshots__/Status.spec.tsx.snap +2 -0
- package/src/components/Chart/ChartSelect/StyledChartSelect.tsx +9 -0
- package/src/components/Chart/ChartSelect/__tests__/StyledChartSelect.spec.tsx +15 -0
- package/src/components/Chart/ChartSelect/__tests__/__snapshots__/StyledChartSelect.spec.tsx.snap +20 -0
- package/src/components/Chart/ChartSelect/__tests__/index.spec.tsx +111 -0
- package/src/components/Chart/ChartSelect/index.tsx +137 -0
- package/src/components/Chart/ColumnChart/ColumnChartContent.tsx +84 -0
- package/src/components/Chart/ColumnChart/Segment.tsx +66 -0
- package/src/components/Chart/ColumnChart/StackedSegment.tsx +99 -0
- package/src/components/Chart/ColumnChart/StyledColumnChart.tsx +9 -0
- package/src/components/Chart/ColumnChart/__tests__/ColumnChartContent.spec.tsx +68 -0
- package/src/components/Chart/ColumnChart/__tests__/Segment.spec.tsx +99 -0
- package/src/components/Chart/ColumnChart/__tests__/StackedSegment.spec.tsx +115 -0
- package/src/components/Chart/ColumnChart/__tests__/__snapshots__/StackedSegment.spec.tsx.snap +120 -0
- package/src/components/Chart/ColumnChart/__tests__/__snapshots__/index.spec.tsx.snap +1405 -0
- package/src/components/Chart/ColumnChart/__tests__/index.spec.tsx +134 -0
- package/src/components/Chart/ColumnChart/index.tsx +216 -0
- package/src/components/Chart/Line/Line.tsx +81 -0
- package/src/components/Chart/Line/__tests__/Line.spec.tsx +148 -0
- package/src/components/Chart/Line/__tests__/__snapshots__/Line.spec.tsx.snap +56 -0
- package/src/components/Chart/Line/__tests__/__snapshots__/index.spec.tsx.snap +1461 -0
- package/src/components/Chart/Line/__tests__/index.spec.tsx +112 -0
- package/src/components/Chart/Line/index.tsx +143 -0
- package/src/components/Chart/StyledChart.tsx +16 -0
- package/src/components/Chart/index.tsx +13 -0
- package/src/components/Chart/shared/AxisLabel.tsx +25 -0
- package/src/components/Chart/shared/ChartFrame.tsx +131 -0
- package/src/components/Chart/shared/ChartHeader.tsx +19 -0
- package/src/components/Chart/shared/EmptyState.tsx +83 -0
- package/src/components/Chart/shared/XAxis.tsx +69 -0
- package/src/components/Chart/shared/XAxisGrid.tsx +42 -0
- package/src/components/Chart/shared/YAxis.tsx +104 -0
- package/src/components/Chart/shared/YAxisGrid.tsx +58 -0
- package/src/components/Chart/shared/__tests__/ChartFrame.spec.tsx +125 -0
- package/src/components/Chart/shared/__tests__/ChartHeader.spec.tsx +22 -0
- package/src/components/Chart/shared/__tests__/EmptyState.spec.tsx +29 -0
- package/src/components/Chart/shared/__tests__/XAXisGrid.spec.tsx +30 -0
- package/src/components/Chart/shared/__tests__/XAxis.spec.tsx +42 -0
- package/src/components/Chart/shared/__tests__/YAxis.spec.tsx +72 -0
- package/src/components/Chart/shared/__tests__/YAxisGrid.spec.tsx +35 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/ChartFrame.spec.tsx.snap +3058 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/ChartHeader.spec.tsx.snap +160 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/EmptyState.spec.tsx.snap +155 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/XAXisGrid.spec.tsx.snap +197 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/XAxis.spec.tsx.snap +369 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/YAxis.spec.tsx.snap +1013 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/YAxisGrid.spec.tsx.snap +228 -0
- package/src/components/Chart/shared/__tests__/niceNumbers.spec.tsx +127 -0
- package/src/components/Chart/shared/constants.ts +2 -0
- package/src/components/Chart/shared/hooks/useColorScale.ts +25 -0
- package/src/components/Chart/shared/hooks/useGenerateTicks.ts +27 -0
- package/src/components/Chart/shared/hooks/useScaleBandX.ts +17 -0
- package/src/components/Chart/shared/hooks/useScaleLinearY.ts +30 -0
- package/src/components/Chart/shared/niceNumbers.ts +68 -0
- package/src/components/Chart/types.ts +100 -0
- package/src/components/Select/MultiSelect/OptionList.tsx +1 -1
- package/src/components/Select/MultiSelect/index.tsx +2 -6
- package/src/components/Select/MultiSelect/utils.ts +1 -1
- package/src/components/Select/SingleSelect/OptionList.tsx +1 -1
- package/src/components/Select/SingleSelect/index.tsx +2 -7
- package/src/components/Select/__tests__/helpers.spec.tsx +0 -36
- package/src/components/Select/helpers.tsx +0 -75
- package/src/components/Switch/SelectorSwitch/__tests__/__snapshots__/Option.spec.tsx.snap +3 -0
- package/src/components/Switch/SelectorSwitch/__tests__/__snapshots__/index.spec.tsx.snap +1 -0
- package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabs.spec.tsx.snap +3 -0
- package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabsHeader.spec.tsx.snap +2 -0
- package/src/components/Tabs/__tests__/__snapshots__/TabWithBadge.spec.tsx.snap +1 -0
- package/src/components/Tabs/__tests__/__snapshots__/index.spec.tsx.snap +3 -0
- package/src/index.ts +2 -0
- package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +28 -0
- package/src/theme/components/chart.ts +28 -0
- package/src/theme/components/columnChart.ts +15 -0
- package/src/theme/getTheme.ts +6 -0
- package/src/types.ts +4 -0
- package/src/utils/__tests__/helpers.spec.ts +36 -1
- package/src/utils/helpers.ts +76 -1
- package/stats/8.100.0/rn-stats.html +4842 -0
- package/stats/8.99.4/rn-stats.html +1 -1
- package/types/components/Badge/Status.d.ts +0 -1
- package/types/components/Chart/ChartSelect/StyledChartSelect.d.ts +8 -0
- package/types/components/Chart/ChartSelect/index.d.ts +63 -0
- package/types/components/Chart/ColumnChart/ColumnChartContent.d.ts +25 -0
- package/types/components/Chart/ColumnChart/Segment.d.ts +14 -0
- package/types/components/Chart/ColumnChart/StackedSegment.d.ts +28 -0
- package/types/components/Chart/ColumnChart/StyledColumnChart.d.ts +8 -0
- package/types/components/Chart/ColumnChart/index.d.ts +80 -0
- package/types/components/Chart/Line/Line.d.ts +21 -0
- package/types/components/Chart/Line/index.d.ts +35 -0
- package/types/components/Chart/StyledChart.d.ts +9 -0
- package/types/components/Chart/index.d.ts +9 -0
- package/types/components/Chart/shared/AxisLabel.d.ts +7 -0
- package/types/components/Chart/shared/ChartFrame.d.ts +30 -0
- package/types/components/Chart/shared/ChartHeader.d.ts +8 -0
- package/types/components/Chart/shared/EmptyState.d.ts +8 -0
- package/types/components/Chart/shared/XAxis.d.ts +8 -0
- package/types/components/Chart/shared/XAxisGrid.d.ts +8 -0
- package/types/components/Chart/shared/YAxis.d.ts +10 -0
- package/types/components/Chart/shared/YAxisGrid.d.ts +10 -0
- package/types/components/Chart/shared/constants.d.ts +2 -0
- package/types/components/Chart/shared/hooks/useColorScale.d.ts +7 -0
- package/types/components/Chart/shared/hooks/useGenerateTicks.d.ts +6 -0
- package/types/components/Chart/shared/hooks/useScaleBandX.d.ts +8 -0
- package/types/components/Chart/shared/hooks/useScaleLinearY.d.ts +9 -0
- package/types/components/Chart/shared/niceNumbers.d.ts +12 -0
- package/types/components/Chart/types.d.ts +84 -0
- package/types/components/Select/helpers.d.ts +0 -5
- package/types/index.d.ts +2 -1
- package/types/theme/components/chart.d.ts +22 -0
- package/types/theme/components/columnChart.d.ts +10 -0
- package/types/theme/getTheme.d.ts +4 -0
- package/types/types.d.ts +3 -1
- package/types/utils/helpers.d.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hero-design/rn",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.100.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"@emotion/native": "^11.9.3",
|
|
23
23
|
"@emotion/primitives-core": "11.0.0",
|
|
24
24
|
"@emotion/react": "^11.9.3",
|
|
25
|
-
"@hero-design/colors": "8.46.
|
|
25
|
+
"@hero-design/colors": "8.46.1",
|
|
26
|
+
"d3": "^7.8.5",
|
|
26
27
|
"date-fns": "^2.30.0",
|
|
27
28
|
"hero-editor": "^1.15.5",
|
|
28
29
|
"nanoid": "^5.0.9"
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
"@testing-library/jest-native": "^5.4.2",
|
|
77
78
|
"@testing-library/react-hooks": "^8.0.1",
|
|
78
79
|
"@testing-library/react-native": "^9.1.0",
|
|
80
|
+
"@types/d3": "^7.4.3",
|
|
79
81
|
"@types/events": "^3.0.3",
|
|
80
82
|
"@types/jest": "^29.5.3",
|
|
81
83
|
"@types/react": "^18.2.0",
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { mobileVisualisationPalette } from '@hero-design/colors';
|
|
3
3
|
|
|
4
|
+
// Only use maasstrichtBlue colors for avatar stack
|
|
5
|
+
const DEFAULT_COLORS = Object.entries(mobileVisualisationPalette)
|
|
6
|
+
.filter(([key]) => key.startsWith('maasstrichtBlue'))
|
|
7
|
+
.map(([, value]) => value);
|
|
8
|
+
|
|
4
9
|
const shuffleArray = <T>(array: Array<T>): Array<T> =>
|
|
5
10
|
array
|
|
6
11
|
.map((value) => ({ value, sort: Math.random() }))
|
|
@@ -11,9 +16,6 @@ const shuffleArray = <T>(array: Array<T>): Array<T> =>
|
|
|
11
16
|
* Hook that returns a memoized and shuffled array of visualisation colors for Avatar.
|
|
12
17
|
*/
|
|
13
18
|
export const useAvatarColors = () => {
|
|
14
|
-
const shuffledColors = useMemo(
|
|
15
|
-
() => shuffleArray(Object.values(mobileVisualisationPalette)),
|
|
16
|
-
[]
|
|
17
|
-
);
|
|
19
|
+
const shuffledColors = useMemo(() => shuffleArray(DEFAULT_COLORS), []);
|
|
18
20
|
return shuffledColors;
|
|
19
21
|
};
|
|
@@ -13,7 +13,6 @@ export interface StatusProps extends ViewProps {
|
|
|
13
13
|
* Whether the Status Badge is visible.
|
|
14
14
|
*/
|
|
15
15
|
visible?: boolean;
|
|
16
|
-
/**
|
|
17
16
|
/**
|
|
18
17
|
* Visual intent color to apply to Status Badge.
|
|
19
18
|
*/
|
|
@@ -71,6 +70,7 @@ const Status = ({
|
|
|
71
70
|
],
|
|
72
71
|
}}
|
|
73
72
|
themeIntent={intent}
|
|
73
|
+
testID="status-dot"
|
|
74
74
|
/>
|
|
75
75
|
</View>
|
|
76
76
|
);
|
|
@@ -24,4 +24,24 @@ describe('Status Badge', () => {
|
|
|
24
24
|
expect(toJSON()).toMatchSnapshot();
|
|
25
25
|
expect(getByText('Activity')).toBeDefined();
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
it.each`
|
|
29
|
+
visible | expectedOpacity
|
|
30
|
+
${true} | ${1}
|
|
31
|
+
${false} | ${0}
|
|
32
|
+
`(
|
|
33
|
+
'status-dot opacity when visible is $visible',
|
|
34
|
+
({ visible, expectedOpacity }) => {
|
|
35
|
+
const { getByTestId } = renderWithTheme(
|
|
36
|
+
<Badge.Status visible={visible}>
|
|
37
|
+
<Typography.Body variant="small">Dot Visibility</Typography.Body>
|
|
38
|
+
</Badge.Status>
|
|
39
|
+
);
|
|
40
|
+
const dot = getByTestId('status-dot');
|
|
41
|
+
const style = Array.isArray(dot.props.style)
|
|
42
|
+
? Object.assign({}, ...dot.props.style)
|
|
43
|
+
: dot.props.style;
|
|
44
|
+
expect(style.opacity).toBe(expectedOpacity);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
27
47
|
});
|
|
@@ -43,6 +43,7 @@ exports[`Status Badge renders correctly 1`] = `
|
|
|
43
43
|
"width": 8,
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
testID="status-dot"
|
|
46
47
|
themeIntent="danger"
|
|
47
48
|
/>
|
|
48
49
|
</View>
|
|
@@ -117,6 +118,7 @@ exports[`Status Badge renders correctly with intent 1`] = `
|
|
|
117
118
|
"width": 8,
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
testID="status-dot"
|
|
120
122
|
themeIntent="success"
|
|
121
123
|
/>
|
|
122
124
|
</View>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { StyledHeaderContainer } from '../StyledChartSelect';
|
|
5
|
+
|
|
6
|
+
describe('StyledHeaderContainer', () => {
|
|
7
|
+
it('applies correct styles', () => {
|
|
8
|
+
const { toJSON } = render(
|
|
9
|
+
<StyledHeaderContainer testID="styled-header">
|
|
10
|
+
<Text>Child</Text>
|
|
11
|
+
</StyledHeaderContainer>
|
|
12
|
+
);
|
|
13
|
+
expect(toJSON()).toMatchSnapshot();
|
|
14
|
+
});
|
|
15
|
+
});
|
package/src/components/Chart/ChartSelect/__tests__/__snapshots__/StyledChartSelect.spec.tsx.snap
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`StyledHeaderContainer applies correct styles 1`] = `
|
|
4
|
+
<View
|
|
5
|
+
style={
|
|
6
|
+
[
|
|
7
|
+
{
|
|
8
|
+
"alignItems": "center",
|
|
9
|
+
"flexDirection": "row",
|
|
10
|
+
},
|
|
11
|
+
undefined,
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
testID="styled-header"
|
|
15
|
+
>
|
|
16
|
+
<Text>
|
|
17
|
+
Child
|
|
18
|
+
</Text>
|
|
19
|
+
</View>
|
|
20
|
+
`;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, waitFor, within } from '@testing-library/react-native';
|
|
3
|
+
import renderWithTheme from '../../../../testHelpers/renderWithTheme';
|
|
4
|
+
import ChartSelect from '../index';
|
|
5
|
+
|
|
6
|
+
const options = [
|
|
7
|
+
{ text: 'Option 1', value: '1' },
|
|
8
|
+
{ text: 'Option 2', value: '2' },
|
|
9
|
+
{ text: 'Option 3', value: '3', disabled: true },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe('ChartSelect', () => {
|
|
13
|
+
it('renders with a selected value', () => {
|
|
14
|
+
const { getByText } = renderWithTheme(
|
|
15
|
+
<ChartSelect value="1" options={options} onConfirm={jest.fn()} />
|
|
16
|
+
);
|
|
17
|
+
expect(getByText('Option 1')).toBeVisible();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders with no value selected', () => {
|
|
21
|
+
const { getByText } = renderWithTheme(
|
|
22
|
+
<ChartSelect value={null} options={options} onConfirm={jest.fn()} />
|
|
23
|
+
);
|
|
24
|
+
// Should render empty string or placeholder
|
|
25
|
+
expect(getByText('')).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('opens bottom sheet and selects an option', async () => {
|
|
29
|
+
const onConfirm = jest.fn();
|
|
30
|
+
const { getByText } = renderWithTheme(
|
|
31
|
+
<ChartSelect
|
|
32
|
+
value="1"
|
|
33
|
+
options={options}
|
|
34
|
+
onConfirm={onConfirm}
|
|
35
|
+
bottomSheetConfig={{ header: 'Select Option' }}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
fireEvent.press(getByText('Option 1'));
|
|
39
|
+
await waitFor(() => expect(getByText('Option 2')).toBeVisible());
|
|
40
|
+
expect(getByText('Option 2')).toBeTruthy();
|
|
41
|
+
// verify botomsheet header
|
|
42
|
+
expect(getByText('Select Option')).toBeVisible();
|
|
43
|
+
|
|
44
|
+
fireEvent.press(getByText('Option 2'));
|
|
45
|
+
expect(onConfirm).toHaveBeenCalledWith('2');
|
|
46
|
+
await waitFor(() => expect(getByText('Select Option')).toBeVisible());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('calls onDismiss when bottom sheet is closed', () => {
|
|
50
|
+
const onDismiss = jest.fn();
|
|
51
|
+
const { getByText, getByTestId } = renderWithTheme(
|
|
52
|
+
<ChartSelect
|
|
53
|
+
value="1"
|
|
54
|
+
options={options}
|
|
55
|
+
onConfirm={jest.fn()}
|
|
56
|
+
onDismiss={onDismiss}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
fireEvent.press(getByText('Option 1'));
|
|
60
|
+
// Simulate closing the bottom sheet
|
|
61
|
+
fireEvent(getByTestId('bottomSheet'), 'onRequestClose');
|
|
62
|
+
expect(onDismiss).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not open when disabled', () => {
|
|
66
|
+
const { getByText, queryByText } = renderWithTheme(
|
|
67
|
+
<ChartSelect value="1" options={options} onConfirm={jest.fn()} disabled />
|
|
68
|
+
);
|
|
69
|
+
fireEvent.press(getByText('Option 1'));
|
|
70
|
+
expect(queryByText('Option 2')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders all options', () => {
|
|
74
|
+
const { getByText, getByTestId } = renderWithTheme(
|
|
75
|
+
<ChartSelect value="1" options={options} onConfirm={jest.fn()} />
|
|
76
|
+
);
|
|
77
|
+
fireEvent.press(getByText('Option 1'));
|
|
78
|
+
|
|
79
|
+
const bottomSheet = getByTestId('bottomSheet');
|
|
80
|
+
const bottomSheetQueries = within(bottomSheet);
|
|
81
|
+
|
|
82
|
+
expect(bottomSheetQueries.getByText('Option 1')).toBeVisible();
|
|
83
|
+
expect(bottomSheetQueries.getByText('Option 2')).toBeVisible();
|
|
84
|
+
expect(bottomSheetQueries.getByText('Option 3')).toBeVisible();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('renders custom header from bottomSheetConfig', () => {
|
|
88
|
+
const { getByText } = renderWithTheme(
|
|
89
|
+
<ChartSelect
|
|
90
|
+
value="1"
|
|
91
|
+
options={options}
|
|
92
|
+
onConfirm={jest.fn()}
|
|
93
|
+
bottomSheetConfig={{ header: 'Custom Header' }}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
fireEvent.press(getByText('Option 1'));
|
|
97
|
+
expect(getByText('Custom Header')).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('applies testID to the correct element', () => {
|
|
101
|
+
const { getByTestId } = renderWithTheme(
|
|
102
|
+
<ChartSelect
|
|
103
|
+
value="1"
|
|
104
|
+
options={options}
|
|
105
|
+
onConfirm={jest.fn()}
|
|
106
|
+
testID="chart-select"
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
expect(getByTestId('chart-select')).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { FlatList, TouchableOpacity, View } from 'react-native';
|
|
4
|
+
import BottomSheet, { BottomSheetProps } from '../../BottomSheet';
|
|
5
|
+
import Typography from '../../Typography';
|
|
6
|
+
import Icon from '../../Icon';
|
|
7
|
+
import List from '../../List';
|
|
8
|
+
import { deepCompareValue, useKeyboard } from '../../../utils/helpers';
|
|
9
|
+
import { StyledHeaderContainer } from './StyledChartSelect';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Represents a selectable option for ChartSelect.
|
|
13
|
+
* @template V The type of the option value.
|
|
14
|
+
* @property value The value of the option. Used for selection and comparison.
|
|
15
|
+
* @property text The display text for the option.
|
|
16
|
+
* @property key (Optional) A unique key for the option. If provided, used as the FlatList key for better performance and stability.
|
|
17
|
+
*/
|
|
18
|
+
type OptionType<V> = {
|
|
19
|
+
value: V;
|
|
20
|
+
text: string;
|
|
21
|
+
key?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface SingleSelectProps<V> {
|
|
25
|
+
/**
|
|
26
|
+
* The currently selected value. Should match one of the values in the options array.
|
|
27
|
+
*/
|
|
28
|
+
value: V | null;
|
|
29
|
+
/**
|
|
30
|
+
* Callback fired when an option is selected.
|
|
31
|
+
* @param value The value of the selected option.
|
|
32
|
+
*/
|
|
33
|
+
onConfirm: (value: V) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Array of supported orientations for the Select modal (iOS only).
|
|
36
|
+
* Defaults to ['portrait'].
|
|
37
|
+
*/
|
|
38
|
+
supportedOrientations?: ('portrait' | 'landscape')[];
|
|
39
|
+
/**
|
|
40
|
+
* The list of selectable options. Each option should have a value and display text.
|
|
41
|
+
*/
|
|
42
|
+
options: OptionType<V>[];
|
|
43
|
+
/**
|
|
44
|
+
* Callback fired when the selection modal is dismissed without selecting an option.
|
|
45
|
+
*/
|
|
46
|
+
onDismiss?: () => void;
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for the bottom sheet modal.
|
|
49
|
+
* - variant: The visual variant of the bottom sheet.
|
|
50
|
+
* - header: Optional header text to display at the top of the bottom sheet.
|
|
51
|
+
*/
|
|
52
|
+
bottomSheetConfig?: {
|
|
53
|
+
variant?: BottomSheetProps['variant'];
|
|
54
|
+
header?: BottomSheetProps['header'];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* If true, the select is disabled and cannot be interacted with.
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Additional style to apply to the select container.
|
|
63
|
+
*/
|
|
64
|
+
style?: StyleProp<ViewStyle>;
|
|
65
|
+
/**
|
|
66
|
+
* Optional test ID for testing purposes. Applied to the main touchable area.
|
|
67
|
+
*/
|
|
68
|
+
testID?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const SingleSelect = <V,>({
|
|
72
|
+
onConfirm,
|
|
73
|
+
onDismiss,
|
|
74
|
+
options = [],
|
|
75
|
+
disabled = false,
|
|
76
|
+
style,
|
|
77
|
+
testID,
|
|
78
|
+
value,
|
|
79
|
+
supportedOrientations = ['portrait'],
|
|
80
|
+
bottomSheetConfig = {},
|
|
81
|
+
}: SingleSelectProps<V>) => {
|
|
82
|
+
const { isKeyboardVisible, keyboardHeight } = useKeyboard();
|
|
83
|
+
const [open, setOpen] = useState(false);
|
|
84
|
+
|
|
85
|
+
const displayedValue =
|
|
86
|
+
options.find((opt) => deepCompareValue(opt.value, value))?.text || '';
|
|
87
|
+
|
|
88
|
+
const { variant: bottomSheetVariant, header: bottomSheetHeader = '' } =
|
|
89
|
+
bottomSheetConfig;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<View pointerEvents={disabled ? 'none' : 'auto'} style={style}>
|
|
94
|
+
<TouchableOpacity onPress={() => setOpen(true)}>
|
|
95
|
+
<StyledHeaderContainer pointerEvents="none" testID={testID}>
|
|
96
|
+
<Typography.Body variant="small-bold">
|
|
97
|
+
{displayedValue}
|
|
98
|
+
</Typography.Body>
|
|
99
|
+
<Icon icon="arrow-down" intent="primary" size="small" />
|
|
100
|
+
</StyledHeaderContainer>
|
|
101
|
+
</TouchableOpacity>
|
|
102
|
+
</View>
|
|
103
|
+
|
|
104
|
+
<BottomSheet
|
|
105
|
+
variant={bottomSheetVariant || 'fixed'}
|
|
106
|
+
open={open}
|
|
107
|
+
onRequestClose={() => {
|
|
108
|
+
onDismiss?.();
|
|
109
|
+
setOpen(false);
|
|
110
|
+
}}
|
|
111
|
+
header={bottomSheetHeader}
|
|
112
|
+
style={{
|
|
113
|
+
paddingBottom: isKeyboardVisible ? keyboardHeight : 0,
|
|
114
|
+
}}
|
|
115
|
+
supportedOrientations={supportedOrientations}
|
|
116
|
+
testID="bottomSheet"
|
|
117
|
+
>
|
|
118
|
+
<FlatList
|
|
119
|
+
data={options}
|
|
120
|
+
keyExtractor={(item) => item.key ?? String(item.value)}
|
|
121
|
+
renderItem={({ item }) => (
|
|
122
|
+
<List.BasicItem
|
|
123
|
+
selected={deepCompareValue(item.value, value)}
|
|
124
|
+
title={item.text}
|
|
125
|
+
onPress={() => {
|
|
126
|
+
setOpen(false);
|
|
127
|
+
onConfirm?.(item.value);
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
</BottomSheet>
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default SingleSelect;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ColumnChartContent.tsx
|
|
2
|
+
// This component renders the grouped/stacked columns for a column chart, using StackedSegment for each x-axis category.
|
|
3
|
+
// It handles the layout and mapping of data series to visual columns.
|
|
4
|
+
|
|
5
|
+
import React, { memo, useMemo } from 'react';
|
|
6
|
+
import { G } from 'react-native-svg';
|
|
7
|
+
import { DataValue, Series, XAxisConfig, YAxisConfig } from '../types';
|
|
8
|
+
import useScaleBandX from '../shared/hooks/useScaleBandX';
|
|
9
|
+
import StackedSegment from './StackedSegment';
|
|
10
|
+
import { deepCompareValue } from '../../../utils/helpers';
|
|
11
|
+
|
|
12
|
+
interface ColumnChartContentProps {
|
|
13
|
+
coordinates: { yStart: number; yEnd: number; xStart: number; xEnd: number };
|
|
14
|
+
data: Array<Series<Array<DataValue>>>;
|
|
15
|
+
yAxisConfig: Omit<YAxisConfig, 'minValue'>;
|
|
16
|
+
xAxisConfig: XAxisConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Called when a bar (column segment) is pressed.
|
|
19
|
+
*/
|
|
20
|
+
onBarPress?: (info: {
|
|
21
|
+
value: number | undefined;
|
|
22
|
+
xLabel: string;
|
|
23
|
+
seriesLabel: string;
|
|
24
|
+
seriesIndex: number;
|
|
25
|
+
xIndex: number;
|
|
26
|
+
}) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Renders the grouped/stacked columns for a column chart.
|
|
31
|
+
* For each x-axis category, renders a StackedSegment with the values from all series.
|
|
32
|
+
* Handles layout and mapping of data to visual columns.
|
|
33
|
+
*/
|
|
34
|
+
const ColumnChartContent = ({
|
|
35
|
+
coordinates,
|
|
36
|
+
data,
|
|
37
|
+
yAxisConfig,
|
|
38
|
+
xAxisConfig,
|
|
39
|
+
onBarPress,
|
|
40
|
+
}: ColumnChartContentProps) => {
|
|
41
|
+
const { yStart, yEnd, xStart, xEnd } = coordinates;
|
|
42
|
+
|
|
43
|
+
const xLabels = xAxisConfig.labels ?? [];
|
|
44
|
+
|
|
45
|
+
// Render columns (fixed width, center of group/column aligns to grid)
|
|
46
|
+
const scaleX = useScaleBandX({
|
|
47
|
+
labels: xAxisConfig.labels ?? [],
|
|
48
|
+
xStart,
|
|
49
|
+
xEnd,
|
|
50
|
+
});
|
|
51
|
+
const columns = useMemo(() => {
|
|
52
|
+
if (data.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return xLabels.flatMap((_, xIdx) => {
|
|
56
|
+
const stackedData = data.map((series) => {
|
|
57
|
+
return series.data[xIdx] ?? undefined;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const x = scaleX(xLabels[xIdx]);
|
|
61
|
+
if (!x) {
|
|
62
|
+
return null; // Handle case where x is undefined
|
|
63
|
+
}
|
|
64
|
+
const seriesLabels = data.map((series) => series.label);
|
|
65
|
+
const xLabel = xLabels[xIdx];
|
|
66
|
+
return (
|
|
67
|
+
<StackedSegment
|
|
68
|
+
key={`${xLabel}-series`}
|
|
69
|
+
stackedData={stackedData}
|
|
70
|
+
xLabel={xLabel}
|
|
71
|
+
yAxisConfig={yAxisConfig}
|
|
72
|
+
coordinates={coordinates}
|
|
73
|
+
xCenter={x + scaleX.bandwidth() / 2}
|
|
74
|
+
seriesLabels={seriesLabels}
|
|
75
|
+
xIndex={xIdx}
|
|
76
|
+
onBarPress={onBarPress}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}, [xLabels, data, xStart, yStart, xEnd, yEnd, onBarPress, yAxisConfig]);
|
|
81
|
+
|
|
82
|
+
return <G testID="column-chart-content">{columns}</G>;
|
|
83
|
+
};
|
|
84
|
+
export default memo(ColumnChartContent, deepCompareValue);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Segment.tsx
|
|
2
|
+
// This component renders a single column segment (bar) for a column chart, with support for accessibility and testID.
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Rect } from 'react-native-svg';
|
|
6
|
+
import { useTheme } from '../../../theme';
|
|
7
|
+
|
|
8
|
+
interface SegmentProps {
|
|
9
|
+
xCenter: number;
|
|
10
|
+
y: number;
|
|
11
|
+
height: number;
|
|
12
|
+
color?: string;
|
|
13
|
+
value?: number;
|
|
14
|
+
xLabel: string;
|
|
15
|
+
seriesLabel: string;
|
|
16
|
+
testID: string;
|
|
17
|
+
onPress?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Renders a single column segment (bar) for a column chart.
|
|
22
|
+
* Applies theming, segment gap, and accessibility label.
|
|
23
|
+
* Used by StackedSegment and other chart content components.
|
|
24
|
+
*/
|
|
25
|
+
const Segment = ({
|
|
26
|
+
xCenter,
|
|
27
|
+
y,
|
|
28
|
+
height,
|
|
29
|
+
color,
|
|
30
|
+
value,
|
|
31
|
+
xLabel,
|
|
32
|
+
seriesLabel,
|
|
33
|
+
testID,
|
|
34
|
+
onPress,
|
|
35
|
+
}: SegmentProps) => {
|
|
36
|
+
const theme = useTheme();
|
|
37
|
+
const { segmentGap } = theme.__hd__.columnChart.space;
|
|
38
|
+
const width = theme.__hd__.columnChart.sizes.columnWidth;
|
|
39
|
+
|
|
40
|
+
// Apply segment gap logic
|
|
41
|
+
const minBarHeight = 10;
|
|
42
|
+
let adjustedHeight = height - segmentGap;
|
|
43
|
+
if (height > 0 && adjustedHeight < minBarHeight) {
|
|
44
|
+
adjustedHeight = minBarHeight;
|
|
45
|
+
} else if (height <= 0) {
|
|
46
|
+
adjustedHeight = 0;
|
|
47
|
+
}
|
|
48
|
+
const adjustedY = y + segmentGap / 2;
|
|
49
|
+
const x = xCenter - width / 2;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Rect
|
|
53
|
+
x={x}
|
|
54
|
+
y={adjustedY}
|
|
55
|
+
width={width}
|
|
56
|
+
height={adjustedHeight}
|
|
57
|
+
rx={width / 2}
|
|
58
|
+
fill={color}
|
|
59
|
+
accessibilityLabel={`Column segment: value ${value}, x-label ${xLabel}, series ${seriesLabel}`}
|
|
60
|
+
testID={testID}
|
|
61
|
+
onPress={onPress}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default React.memo(Segment);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// StackedSegment.tsx
|
|
2
|
+
// This component renders a stack of column segments for a single x-axis category in a grouped/stacked column chart.
|
|
3
|
+
// Each segment represents a value from a different series, stacked vertically. Uses the Segment component for rendering each bar.
|
|
4
|
+
|
|
5
|
+
import React, { memo } from 'react';
|
|
6
|
+
import Segment from './Segment';
|
|
7
|
+
import { DataValue, YAxisConfig } from '../types';
|
|
8
|
+
import useScaleLinearY from '../shared/hooks/useScaleLinearY';
|
|
9
|
+
import useColorScale from '../shared/hooks/useColorScale';
|
|
10
|
+
import { deepCompareValue } from '../../../utils/helpers';
|
|
11
|
+
|
|
12
|
+
interface StackedSegmentProps {
|
|
13
|
+
stackedData: Array<DataValue>;
|
|
14
|
+
seriesLabels: Array<string>;
|
|
15
|
+
xLabel: string;
|
|
16
|
+
yAxisConfig: YAxisConfig;
|
|
17
|
+
coordinates: { yStart: number; yEnd: number; xStart: number; xEnd: number };
|
|
18
|
+
xCenter: number;
|
|
19
|
+
xIndex: number;
|
|
20
|
+
/**
|
|
21
|
+
* Called when a bar (column segment) is pressed.
|
|
22
|
+
*/
|
|
23
|
+
onBarPress?: (info: {
|
|
24
|
+
value: number | undefined;
|
|
25
|
+
xLabel: string;
|
|
26
|
+
seriesLabel: string;
|
|
27
|
+
seriesIndex: number;
|
|
28
|
+
xIndex: number;
|
|
29
|
+
}) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Renders a stack of column segments for a single x-axis category.
|
|
34
|
+
* Each segment corresponds to a value in stackedData and is colored by series.
|
|
35
|
+
* Skips undefined values. Used for grouped/stacked column charts.
|
|
36
|
+
*/
|
|
37
|
+
const StackedSegment: React.FC<StackedSegmentProps> = ({
|
|
38
|
+
stackedData,
|
|
39
|
+
seriesLabels,
|
|
40
|
+
xLabel,
|
|
41
|
+
yAxisConfig,
|
|
42
|
+
coordinates,
|
|
43
|
+
xCenter,
|
|
44
|
+
xIndex,
|
|
45
|
+
onBarPress,
|
|
46
|
+
}) => {
|
|
47
|
+
const { yStart, yEnd } = coordinates;
|
|
48
|
+
const stackedMaxY = yAxisConfig.maxValue ?? 0;
|
|
49
|
+
const scaleY = useScaleLinearY({
|
|
50
|
+
maxValue: stackedMaxY,
|
|
51
|
+
minValue: 0,
|
|
52
|
+
yStart,
|
|
53
|
+
yEnd,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let yStack = 0; // running sum for stacking
|
|
57
|
+
const colorScale = useColorScale(seriesLabels);
|
|
58
|
+
return stackedData.map((value, index) => {
|
|
59
|
+
// If value is undefined, skip this segment
|
|
60
|
+
if (value === undefined) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const prevYStack = yStack;
|
|
64
|
+
yStack += value ?? 0;
|
|
65
|
+
|
|
66
|
+
// y0 is the bottom of the stack, y1 is the top
|
|
67
|
+
const y0 = scaleY(prevYStack);
|
|
68
|
+
const y1 = scaleY(yStack);
|
|
69
|
+
const colHeight = y0 - y1; // since y increases downward in SVG
|
|
70
|
+
const seriesLabel = seriesLabels[index];
|
|
71
|
+
return (
|
|
72
|
+
<Segment
|
|
73
|
+
key={`${xLabel}-series-${seriesLabel}`}
|
|
74
|
+
xCenter={xCenter}
|
|
75
|
+
y={y1}
|
|
76
|
+
height={colHeight}
|
|
77
|
+
color={colorScale(seriesLabel)}
|
|
78
|
+
value={value}
|
|
79
|
+
xLabel={xLabel}
|
|
80
|
+
seriesLabel={seriesLabel}
|
|
81
|
+
testID={`column-segment-${xLabel}-${seriesLabel}`}
|
|
82
|
+
onPress={
|
|
83
|
+
onBarPress
|
|
84
|
+
? () =>
|
|
85
|
+
onBarPress({
|
|
86
|
+
value,
|
|
87
|
+
xLabel,
|
|
88
|
+
seriesLabel,
|
|
89
|
+
seriesIndex: index,
|
|
90
|
+
xIndex,
|
|
91
|
+
})
|
|
92
|
+
: undefined
|
|
93
|
+
}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default memo(StackedSegment, deepCompareValue);
|