@hero-design/rn-work-uikit 1.3.1 → 1.5.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 (44) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/index.js +19933 -735
  3. package/package.json +3 -2
  4. package/src/components/DatePicker/__tests__/__snapshots__/index.spec.tsx.snap +3 -3
  5. package/src/components/FormGroup/__tests__/__snapshots__/index.spec.tsx.snap +903 -0
  6. package/src/components/FormGroup/__tests__/index.spec.tsx +306 -0
  7. package/src/components/FormGroup/__tests__/utils.spec.ts +73 -0
  8. package/src/components/FormGroup/index.tsx +106 -0
  9. package/src/components/FormGroup/utils.ts +67 -0
  10. package/src/components/RichTextEditor/EditorEvent.ts +7 -0
  11. package/src/components/RichTextEditor/EditorToolbar.tsx +216 -0
  12. package/src/components/RichTextEditor/MentionList.tsx +99 -0
  13. package/src/components/RichTextEditor/RichTextEditor.tsx +88 -0
  14. package/src/components/RichTextEditor/RichTextEditorInput.tsx +292 -0
  15. package/src/components/RichTextEditor/StyledRichTextEditor.tsx +15 -0
  16. package/src/components/RichTextEditor/StyledToolbar.ts +32 -0
  17. package/src/components/RichTextEditor/__mocks__/hero-editor.js +3 -0
  18. package/src/components/RichTextEditor/__mocks__/heroEditorApp.ts +2 -0
  19. package/src/components/RichTextEditor/__tests__/EditorToolbar.spec.tsx +144 -0
  20. package/src/components/RichTextEditor/__tests__/MentionList.spec.tsx +105 -0
  21. package/src/components/RichTextEditor/__tests__/RichTextEditorInput.spec.tsx +136 -0
  22. package/src/components/RichTextEditor/__tests__/__snapshots__/EditorToolbar.spec.tsx.snap +414 -0
  23. package/src/components/RichTextEditor/__tests__/__snapshots__/MentionList.spec.tsx.snap +13 -0
  24. package/src/components/RichTextEditor/constants.ts +9 -0
  25. package/src/{hero-editor.d.ts → components/RichTextEditor/hero-editor.d.ts} +6 -0
  26. package/src/components/RichTextEditor/heroEditorApp.ts +3 -0
  27. package/src/components/RichTextEditor/index.tsx +20 -0
  28. package/src/components/RichTextEditor/types.ts +87 -0
  29. package/src/components/RichTextEditor/utils/events.ts +31 -0
  30. package/src/components/RichTextEditor/utils/rnWebView.tsx +30 -0
  31. package/src/components/Select/__tests__/__snapshots__/index.spec.tsx.snap +24 -2
  32. package/src/components/Select/index.tsx +11 -10
  33. package/src/components/TextInput/Group/__tests__/__snapshots__/index.spec.tsx.snap +3 -3
  34. package/src/components/TextInput/Group/index.tsx +6 -1
  35. package/src/components/TextInput/InputComponent.tsx +59 -18
  36. package/src/components/TextInput/InputRow.tsx +13 -7
  37. package/src/components/TextInput/StyledTextInput.tsx +3 -3
  38. package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +17 -17
  39. package/src/components/TextInput/index.tsx +22 -13
  40. package/src/components/TextInput/types.ts +30 -5
  41. package/src/index.ts +3 -1
  42. package/src/utils/hooks.ts +10 -0
  43. package/stats/1.5.0/rn-work-uikit-stats.html +4844 -0
  44. package/stats/1.3.1/rn-work-uikit-stats.html +0 -4844
@@ -0,0 +1,32 @@
1
+ import { styled } from '@hero-design/rn';
2
+ import { StyleSheet, TouchableOpacity, View } from 'react-native';
3
+
4
+ export const StyledToolbarButton = styled(TouchableOpacity)<{
5
+ selected: boolean;
6
+ }>(({ theme, selected }) => ({
7
+ width: theme.__hd__.richTextEditor.sizes.toolbarButtonSize,
8
+ height: theme.__hd__.richTextEditor.sizes.toolbarButtonSize,
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ backgroundColor: selected
12
+ ? theme.__hd__.richTextEditor.colors.toolbarButtonSelectedBackground
13
+ : undefined,
14
+ }));
15
+
16
+ export const StyledToolbar = styled(View)(({ theme }) => ({
17
+ flexDirection: 'row',
18
+ alignItems: 'center',
19
+ borderTopWidth: StyleSheet.hairlineWidth,
20
+ borderTopColor: theme.__hd__.richTextEditor.colors.toolbarBorderColor,
21
+ backgroundColor: theme.__hd__.richTextEditor.colors.toolbarBackgroundColor,
22
+ paddingHorizontal: theme.__hd__.richTextEditor.space.toolbarHorizontalPadding,
23
+ }));
24
+
25
+ export const StyledSeparator = styled(View)(({ theme }) => ({
26
+ width: theme.__hd__.richTextEditor.sizes.toolbarSeparatorWidth,
27
+ height: theme.__hd__.richTextEditor.sizes.toolbarSeparatorHeight,
28
+ flexDirection: 'row',
29
+ alignItems: 'center',
30
+ marginHorizontal: theme.space.small,
31
+ backgroundColor: theme.colors.secondaryOutline,
32
+ }));
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ plainSerializer: () => 'mocked',
3
+ };
@@ -0,0 +1,2 @@
1
+ const mockHeroEditApp = 'mockHeroEditApp';
2
+ export default mockHeroEditApp;
@@ -0,0 +1,144 @@
1
+ import React from 'react';
2
+ import { act, fireEvent, waitFor } from '@testing-library/react-native';
3
+ import type { RenderAPI } from '@testing-library/react-native';
4
+ import EditorToolbar from '../EditorToolbar';
5
+ import { emitter as editorEventEmmitor } from '../EditorEvent';
6
+ import { theme, ThemeProvider } from '../../../index';
7
+ import * as Events from '../utils/events';
8
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
9
+
10
+ describe('EditorToolbar', () => {
11
+ it('should not render toolbar when the editor is not focused', () => {
12
+ const wrapper = renderWithTheme(
13
+ <EditorToolbar testID="editor-toolbar" name="toolbar" />
14
+ );
15
+ const toolbar = wrapper.queryByTestId('editor-toolbar');
16
+ expect(toolbar).toBeNull();
17
+ });
18
+
19
+ describe('when the editor is focused', () => {
20
+ let wrapper: RenderAPI;
21
+
22
+ beforeEach(async () => {
23
+ wrapper = renderWithTheme(
24
+ <EditorToolbar name="toolbar" testID="toolbar" />
25
+ );
26
+ act(() => {
27
+ editorEventEmmitor.emit('toolbar/editor-focus');
28
+ });
29
+ await waitFor(() => wrapper.getByTestId('toolbar'));
30
+ });
31
+
32
+ it('should render toolbar', async () => {
33
+ expect(wrapper.toJSON()).toMatchSnapshot();
34
+
35
+ [
36
+ 'format-bold',
37
+ 'format-italic',
38
+ 'format-underlined',
39
+ 'format-list-bulleted',
40
+ 'format-list-numbered',
41
+ 'format-heading1',
42
+ 'format-heading2',
43
+ ].forEach((testId) => {
44
+ expect(wrapper.queryAllByTestId(testId)).toHaveLength(1);
45
+ });
46
+ });
47
+
48
+ it('should hide toolbar when blur', async () => {
49
+ act(() => {
50
+ editorEventEmmitor.emit('toolbar/editor-blur');
51
+ });
52
+ await waitFor(() => wrapper.queryAllByTestId('toolbar').length === 0);
53
+ expect(wrapper.queryAllByTestId('toolbar')).toHaveLength(0);
54
+ });
55
+
56
+ describe("should change button's background color when pressing", () => {
57
+ it('should send event and highlight buttons correctly', async () => {
58
+ const emmitedEvents: string[] = [];
59
+
60
+ const eventNameAndTestIDArray = [
61
+ {
62
+ eventName: 'toolbar/bold',
63
+ testID: 'format-bold',
64
+ },
65
+ {
66
+ eventName: 'toolbar/italic',
67
+ testID: 'format-italic',
68
+ },
69
+ {
70
+ eventName: 'toolbar/underline',
71
+ testID: 'format-underlined',
72
+ },
73
+ {
74
+ eventName: 'toolbar/bulleted-list',
75
+ testID: 'format-list-bulleted',
76
+ },
77
+ {
78
+ eventName: 'toolbar/numbered-list',
79
+ testID: 'format-list-numbered',
80
+ },
81
+ {
82
+ eventName: 'toolbar/heading-one',
83
+ testID: 'format-heading1',
84
+ },
85
+ {
86
+ eventName: 'toolbar/heading-two',
87
+ testID: 'format-heading2',
88
+ },
89
+ ];
90
+
91
+ eventNameAndTestIDArray.forEach(async ({ eventName, testID }) => {
92
+ Events.on(editorEventEmmitor, eventName, () => {
93
+ emmitedEvents.push(testID);
94
+ });
95
+
96
+ expect(wrapper.getByTestId(testID)).toHaveStyle({
97
+ backgroundColor: undefined,
98
+ });
99
+
100
+ await act(() => {
101
+ fireEvent.press(wrapper.getByTestId(testID));
102
+ });
103
+
104
+ // rerender
105
+ wrapper.rerender(
106
+ <ThemeProvider theme={theme}>
107
+ <EditorToolbar name="toolbar" testID="toolbar" />
108
+ </ThemeProvider>
109
+ );
110
+ // match snapshot
111
+ // add waitfor to ensure the style is applied
112
+ await waitFor(() => {
113
+ expect(wrapper.getByTestId(testID)).toHaveStyle({
114
+ backgroundColor:
115
+ theme.__hd__.richTextEditor.colors
116
+ .toolbarButtonSelectedBackground,
117
+ });
118
+ });
119
+ });
120
+
121
+ const standaloneButtonTestIDs = [
122
+ 'format-list-bulleted',
123
+ 'format-list-numbered',
124
+ 'format-heading1',
125
+ ];
126
+ standaloneButtonTestIDs.forEach((testID) => {
127
+ expect(wrapper.getByTestId(testID)).toHaveStyle({
128
+ backgroundColor: undefined,
129
+ });
130
+ });
131
+
132
+ expect(emmitedEvents).toMatchObject([
133
+ 'format-bold',
134
+ 'format-italic',
135
+ 'format-underlined',
136
+ 'format-list-bulleted',
137
+ 'format-list-numbered',
138
+ 'format-heading1',
139
+ 'format-heading2',
140
+ ]);
141
+ });
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,105 @@
1
+ import type { RenderAPI } from '@testing-library/react-native';
2
+ import { act, fireEvent, render } from '@testing-library/react-native';
3
+ import React from 'react';
4
+ import { Text, View } from 'react-native';
5
+ import { emitter as editorEventEmitter } from '../EditorEvent';
6
+ import MentionList from '../MentionList';
7
+ import * as Events from '../utils/events';
8
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
9
+
10
+ const SuggestionListData: Array<{ id: string; name: string }> = [
11
+ { id: '1', name: 'Kien Tran' },
12
+ { id: '2', name: 'Minh Dinh' },
13
+ { id: '3', name: 'Hieu Pham' },
14
+ { id: '4', name: 'Hau' },
15
+ { id: '5', name: 'Tung Ten' },
16
+ { id: '6', name: 'Thong Quach' },
17
+ ];
18
+
19
+ const SuggestionList = ({
20
+ searchValue,
21
+ onSelect,
22
+ }: {
23
+ searchValue: string;
24
+ onSelect: (id: string, name: string) => void;
25
+ }) => {
26
+ const filteredData = SuggestionListData.filter((item) =>
27
+ item.name.includes(searchValue)
28
+ );
29
+
30
+ return (
31
+ <View>
32
+ {filteredData.map((item) => (
33
+ <Text key={item.id} onPress={() => onSelect(item.id, item.name)}>
34
+ {item.name}
35
+ </Text>
36
+ ))}
37
+ </View>
38
+ );
39
+ };
40
+
41
+ describe('MentionList', () => {
42
+ describe('when search string is empty', () => {
43
+ it('should not render mention list', () => {
44
+ const wrapper = render(
45
+ <MentionList
46
+ name="give-shout-out"
47
+ render={() => <Text>Mention List</Text>}
48
+ />
49
+ );
50
+
51
+ expect(wrapper.toJSON()).toBeNull();
52
+ });
53
+ });
54
+
55
+ describe('when search string is not empty', () => {
56
+ let wrapper: RenderAPI;
57
+
58
+ beforeEach(async () => {
59
+ wrapper = renderWithTheme(
60
+ <MentionList
61
+ name="give-shout-out"
62
+ render={(
63
+ searchValue: string,
64
+ onSelect: (id: string, name: string) => void
65
+ ) => <SuggestionList searchValue={searchValue} onSelect={onSelect} />}
66
+ />
67
+ );
68
+ await act(() => {
69
+ editorEventEmitter.emit('give-shout-out/mention-search', {
70
+ search: 'Hieu',
71
+ target: 'give-shout-out',
72
+ });
73
+ });
74
+ });
75
+
76
+ it('should render mention list showing filtered results', async () => {
77
+ expect(wrapper.toJSON()).toMatchSnapshot();
78
+ expect(wrapper.queryAllByText('Hieu Pham')).toHaveLength(1);
79
+ expect(wrapper.queryAllByText('Kien Tran')).toHaveLength(0);
80
+ expect(wrapper.queryAllByText('Minh Dinh')).toHaveLength(0);
81
+ expect(wrapper.queryAllByText('Hau')).toHaveLength(0);
82
+ expect(wrapper.queryAllByText('Tung Teng')).toHaveLength(0);
83
+ expect(wrapper.queryAllByText('Thong Quach')).toHaveLength(0);
84
+ });
85
+
86
+ describe('onPress suggesstion item', () => {
87
+ it('should emit action metion-only', () => {
88
+ const result: { id: string; name: string; target: string }[] = [];
89
+ act(() => {
90
+ Events.on(
91
+ editorEventEmitter,
92
+ 'give-shout-out/mention-apply',
93
+ (data: { id: string; name: string; target: string }) => {
94
+ result.push(data);
95
+ }
96
+ );
97
+ });
98
+ fireEvent(wrapper.getByText('Hieu Pham'), 'press');
99
+ expect(result).toMatchObject([
100
+ { id: '3', name: 'Hieu Pham', target: 'give-shout-out' },
101
+ ]);
102
+ });
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,136 @@
1
+ import React from 'react';
2
+ import { fireEvent } from '@testing-library/react-native';
3
+ import RichTextEditorInput from '../RichTextEditorInput';
4
+ import { emitter } from '../EditorEvent';
5
+ import * as Events from '../utils/events';
6
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
7
+
8
+ // Mock minimal EditorValue type for test
9
+ const initialValue = [
10
+ { type: 'paragraph', children: [{ text: 'Hello world' }] },
11
+ ];
12
+
13
+ describe('RichTextEditorInput', () => {
14
+ it('renders a WebView with correct initial html', () => {
15
+ const { getByTestId } = renderWithTheme(
16
+ <RichTextEditorInput
17
+ value={initialValue}
18
+ name="test-editor"
19
+ autoFocus={false}
20
+ placeholder="Type something..."
21
+ onChange={jest.fn()}
22
+ />
23
+ );
24
+
25
+ const webview = getByTestId('webview');
26
+ expect(webview).toBeTruthy();
27
+ expect(webview.props.source.html).toContain(
28
+ 'placeholder: "Type something..."'
29
+ );
30
+ expect(webview.props.source.html).toContain(JSON.stringify(initialValue));
31
+ });
32
+
33
+ it('handles editor-change event from webview', () => {
34
+ const onChange = jest.fn();
35
+ const { getByTestId } = renderWithTheme(
36
+ <RichTextEditorInput
37
+ value={initialValue}
38
+ name="test-editor"
39
+ autoFocus={false}
40
+ placeholder="Type here..."
41
+ onChange={onChange}
42
+ />
43
+ );
44
+
45
+ const webview = getByTestId('webview');
46
+ const newValue = [{ type: 'paragraph', children: [{ text: 'Changed' }] }];
47
+ fireEvent(webview, 'message', {
48
+ nativeEvent: {
49
+ data: JSON.stringify({
50
+ type: '@hero-editor/webview/editor-change',
51
+ data: { value: newValue },
52
+ }),
53
+ },
54
+ });
55
+
56
+ expect(onChange).toHaveBeenCalledWith(newValue);
57
+ });
58
+
59
+ it('handles editor-focus and editor-blur events from webview', () => {
60
+ const onFocus = jest.fn();
61
+ const onBlur = jest.fn();
62
+ const { getByTestId } = renderWithTheme(
63
+ <RichTextEditorInput
64
+ value={initialValue}
65
+ name="test-editor"
66
+ autoFocus={false}
67
+ placeholder="Type here..."
68
+ onChange={jest.fn()}
69
+ onFocus={onFocus}
70
+ onBlur={onBlur}
71
+ />
72
+ );
73
+
74
+ const webview = getByTestId('webview');
75
+
76
+ // Spy on emitter
77
+ const spy = jest.spyOn(Events, 'emit');
78
+
79
+ // Simulate focus
80
+ fireEvent(webview, 'message', {
81
+ nativeEvent: {
82
+ data: JSON.stringify({
83
+ type: '@hero-editor/webview/editor-focus',
84
+ }),
85
+ },
86
+ });
87
+ expect(spy).toHaveBeenCalledWith(
88
+ emitter,
89
+ 'test-editor/editor-focus',
90
+ undefined
91
+ );
92
+
93
+ // Simulate blur
94
+ fireEvent(webview, 'message', {
95
+ nativeEvent: {
96
+ data: JSON.stringify({
97
+ type: '@hero-editor/webview/editor-blur',
98
+ }),
99
+ },
100
+ });
101
+ expect(spy).toHaveBeenCalledWith(
102
+ emitter,
103
+ 'test-editor/editor-blur',
104
+ undefined
105
+ );
106
+
107
+ spy.mockRestore();
108
+ });
109
+
110
+ it('handles cursor-change event from webview', () => {
111
+ const onCursorChange = jest.fn();
112
+ const { getByTestId } = renderWithTheme(
113
+ <RichTextEditorInput
114
+ value={initialValue}
115
+ name="test-editor"
116
+ autoFocus={false}
117
+ placeholder="Type here..."
118
+ onChange={jest.fn()}
119
+ onCursorChange={onCursorChange}
120
+ />
121
+ );
122
+
123
+ const webview = getByTestId('webview');
124
+ const cursorData = { position: { top: 10 } };
125
+ fireEvent(webview, 'message', {
126
+ nativeEvent: {
127
+ data: JSON.stringify({
128
+ type: '@hero-editor/webview/cursor-change',
129
+ data: cursorData,
130
+ }),
131
+ },
132
+ });
133
+
134
+ expect(onCursorChange).toHaveBeenCalledWith(cursorData);
135
+ });
136
+ });