@hero-design/rn 8.108.2 → 8.109.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.
@@ -1,21 +1,17 @@
1
- import { useTheme } from '@emotion/react';
2
1
  import { isEmptyContent } from 'hero-editor';
3
2
  import React, {
4
3
  forwardRef,
5
4
  useCallback,
6
5
  useEffect,
7
- useImperativeHandle,
8
6
  useMemo,
9
7
  useRef,
10
8
  useState,
11
9
  } from 'react';
12
10
 
13
- import { Animated, TouchableWithoutFeedback, Easing } from 'react-native';
14
- import type { WebView } from 'react-native-webview';
15
- import type { ComponentType, ReactElement, Ref } from 'react';
16
- import type { StyleProp, ViewStyle, LayoutChangeEvent } from 'react-native';
17
- import heroEditorApp from './heroEditorApp';
18
- import { isAndroid } from '../../utils/helpers';
11
+ import { Animated, Easing } from 'react-native';
12
+ import type { ReactElement, Ref } from 'react';
13
+ import type { LayoutChangeEvent } from 'react-native';
14
+ import { useTheme } from '../../theme';
19
15
  import Icon from '../Icon';
20
16
  import {
21
17
  StyledAsteriskLabelInsideTextInput,
@@ -31,61 +27,21 @@ import {
31
27
  StyledTextInputAndLabelContainer,
32
28
  StyledTextInputContainer,
33
29
  } from '../TextInput/StyledTextInput';
34
- import { ToolbarEvents } from './constants';
35
- import { emitter } from './EditorEvent';
36
- import { StyledWebView } from './StyledRichTextEditor';
37
- import * as Events from './utils/events';
38
- import { postMessage, requestBlurEditor } from './utils/rnWebView';
39
- import type { WebViewEventMessage } from './utils/rnWebView';
40
30
  import { LABEL_ANIMATION_DURATION } from '../TextInput';
41
- import type { TextUnit } from './types';
42
31
  import Typography from '../Typography';
43
-
44
- export interface RichTextEditorRef {
45
- requestBlur: VoidFunction;
46
- insertNodes: (nodes: Record<string, unknown>[]) => void;
47
- deleteBackward: (unit?: TextUnit) => void;
48
- setReadOnly: (readOnly: boolean) => void;
49
- }
32
+ import BaseRichTextEditor from './BaseRichTextEditor';
33
+ import type {
34
+ BaseRichTextEditorProps,
35
+ RichTextEditorRef,
36
+ } from './BaseRichTextEditor';
50
37
 
51
38
  export type EditorValue = {
52
39
  type: string;
53
40
  children: any;
54
41
  }[];
55
42
 
56
- export interface RichTextEditorProps {
57
- /**
58
- * If true, the editor will be focused when the user enters the screen
59
- */
60
- autoFocus?: boolean;
61
- /**
62
- * Error message
63
- */
64
- error?: string;
65
- /**
66
- * Field value
67
- */
68
- value?: EditorValue;
69
- /**
70
- * Unique name used to communicate with webview
71
- */
72
- name: string;
73
- /**
74
- * Callback function called when the field value changed
75
- */
76
- onChange: (data: EditorValue) => void;
77
- /**
78
- * Callback function called when the cursor position changed
79
- */
80
- onCursorChange?: (params: { position: { top: number } }) => void;
81
- /**
82
- * Field placeholder
83
- */
84
- placeholder?: string;
85
- /**
86
- * Additional styles
87
- */
88
- style?: StyleProp<ViewStyle>;
43
+ export interface RichTextEditorProps
44
+ extends Omit<BaseRichTextEditorProps, 'editorRef'> {
89
45
  /**
90
46
  * Field label
91
47
  */
@@ -115,7 +71,7 @@ const defaultValue: EditorValue = [
115
71
  },
116
72
  ];
117
73
 
118
- const RichTextEditor: ComponentType<RichTextEditorProps> = ({
74
+ const RichTextEditor = ({
119
75
  autoFocus = true,
120
76
  name,
121
77
  placeholder = '',
@@ -127,14 +83,13 @@ const RichTextEditor: ComponentType<RichTextEditorProps> = ({
127
83
  helpText,
128
84
  required,
129
85
  testID,
86
+ onFocus,
87
+ onBlur,
130
88
  forwardedRef,
131
89
  value = defaultValue,
132
90
  }: RichTextEditorProps): ReactElement => {
133
91
  const theme = useTheme();
134
92
 
135
- const webview = useRef<WebView | null>(null);
136
- const [webviewHeight, setWebviewHeight] = useState<number | undefined>();
137
-
138
93
  const [isFocused, setIsFocused] = useState(false);
139
94
  const isEmptyValue = useMemo(() => isEmptyContent(value), [value]);
140
95
  const state = useMemo(() => {
@@ -147,12 +102,6 @@ const RichTextEditor: ComponentType<RichTextEditorProps> = ({
147
102
  return 'default';
148
103
  }, [isFocused, error, isEmptyValue]);
149
104
 
150
- const normalizeEventName = (event: string) => `${name}/${event}`;
151
- const postMessageToWebview = (message: WebViewEventMessage) => {
152
- if (webview && webview.current) {
153
- postMessage(webview.current, message);
154
- }
155
- };
156
105
  const [inputSize, setInputSize] = React.useState<{
157
106
  height: number;
158
107
  width: number;
@@ -174,184 +123,15 @@ const RichTextEditor: ComponentType<RichTextEditorProps> = ({
174
123
  setInputSize((prev) => ({ ...prev, height, width }));
175
124
  }, []);
176
125
 
177
- useEffect(() => {
178
- const removeFocusListener = Events.on(
179
- emitter,
180
- normalizeEventName('editor-focus'),
181
- () => setIsFocused(true)
182
- );
183
-
184
- const removeBlurListener = Events.on(
185
- emitter,
186
- normalizeEventName('editor-blur'),
187
- () => setIsFocused(false)
188
- );
189
-
190
- return () => {
191
- removeFocusListener();
192
- removeBlurListener();
193
- };
194
- }, []);
195
-
196
- const html = useMemo(() => {
197
- const initialValueString = JSON.stringify(value);
198
-
199
- return `
200
- <!DOCTYPE html>
201
- <html>
202
- <head>
203
- <meta charset="utf-8">
204
- <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
205
- <style>
206
- body {
207
- margin: 0;
208
- }
209
- </style>
210
- </head>
211
- <body>
212
- <div id="root"></div>
213
- <script>
214
- window.__editorConfigs = {
215
- placeholder: "${placeholder}",
216
- initialValue: ${initialValueString},
217
- isAndroid: ${isAndroid ? 'true' : 'false'},
218
- autoFocus: ${autoFocus},
219
- style: {
220
- padding: '0 !important',
221
- fontSize: ${theme.__hd__.richTextEditor.fontSizes.editor},
222
- color: '${theme.__hd__.richTextEditor.colors.text}'
223
- }
224
- };
225
- ${heroEditorApp}
226
- </script>
227
- </body>
228
- </html>
229
- `;
230
- }, []);
231
-
232
- const requestBlur = useCallback(() => {
233
- if (webview.current && isFocused) {
234
- requestBlurEditor(webview.current);
235
- }
236
- }, [isFocused]);
237
-
238
- const insertNodes = useCallback((nodes: Record<string, unknown>[]) => {
239
- postMessageToWebview({
240
- type: '@hero-editor/webview/editor-insert-nodes',
241
- data: { nodes },
242
- });
243
- }, []);
244
-
245
- const deleteBackward = useCallback((unit?: TextUnit) => {
246
- postMessageToWebview({
247
- type: '@hero-editor/webview/editor-delete-backward',
248
- data: { unit },
249
- });
250
- }, []);
251
-
252
- const setReadOnly = useCallback((readOnly: boolean) => {
253
- postMessageToWebview({
254
- type: '@hero-editor/webview/editor-read-only',
255
- data: { readOnly },
256
- });
257
- }, []);
258
-
259
- useImperativeHandle(
260
- forwardedRef,
261
- () => ({ requestBlur, insertNodes, deleteBackward, setReadOnly }),
262
- [requestBlur, deleteBackward, insertNodes, setReadOnly]
263
- );
264
-
265
- /* Forward events from Toolbar and MentionList to webview */
266
- useEffect(() => {
267
- const toolbarEventListenerRemovers = Object.values(ToolbarEvents).map(
268
- (eventName) =>
269
- Events.on(emitter, normalizeEventName(eventName), (data) => {
270
- postMessageToWebview({
271
- type: `@hero-editor/webview/${eventName}`,
272
- data,
273
- });
274
- })
275
- );
276
-
277
- const removeMentionApplyListener = Events.on(
278
- emitter,
279
- normalizeEventName('mention-apply'),
280
- (data) =>
281
- postMessageToWebview({
282
- type: '@hero-editor/webview/mention-apply',
283
- data,
284
- })
285
- );
286
-
287
- return () => {
288
- removeMentionApplyListener();
289
- toolbarEventListenerRemovers.forEach((remover) => remover());
290
- };
291
- }, []);
292
-
293
- const handleEditorLayoutEvent = useCallback((messageData: any) => {
294
- const editorLayout = messageData
295
- ? {
296
- width: Number(messageData.width),
297
- height: Number(messageData.height),
298
- }
299
- : undefined;
300
-
301
- if (editorLayout) {
302
- setWebviewHeight(editorLayout.height);
303
- }
304
- }, []);
305
-
306
- /* Handle events from webview */
307
- const onMessage = useCallback(
308
- (event?: { nativeEvent?: { data?: string } }) => {
309
- const message = event?.nativeEvent?.data
310
- ? JSON.parse(event?.nativeEvent?.data)
311
- : undefined;
312
-
313
- const messageType = message?.type;
314
-
315
- const messageData = message?.data;
316
-
317
- switch (messageType) {
318
- case '@hero-editor/webview/editor-focus':
319
- Events.emit(emitter, normalizeEventName('editor-focus'), undefined);
320
-
321
- break;
322
- case '@hero-editor/webview/editor-blur':
323
- Events.emit(emitter, normalizeEventName('editor-blur'), undefined);
324
-
325
- break;
326
- case '@hero-editor/webview/mention-search':
327
- Events.emit(
328
- emitter,
329
- normalizeEventName('mention-search'),
330
- messageData
331
- );
126
+ const handleEditorFocus = useCallback(() => {
127
+ onFocus?.();
128
+ setIsFocused(true);
129
+ }, [onFocus]);
332
130
 
333
- break;
334
- case '@hero-editor/webview/editor-change':
335
- if (messageData) {
336
- onChange(messageData.value);
337
- }
338
-
339
- break;
340
- case '@hero-editor/webview/cursor-change':
341
- onCursorChange?.(messageData);
342
-
343
- break;
344
-
345
- case '@hero-editor/webview/editor-layout':
346
- handleEditorLayoutEvent(messageData);
347
- break;
348
-
349
- default:
350
- break;
351
- }
352
- },
353
- []
354
- );
131
+ const handleEditorBlur = useCallback(() => {
132
+ onBlur?.();
133
+ setIsFocused(false);
134
+ }, [onBlur]);
355
135
 
356
136
  return (
357
137
  <StyledContainer testID={testID}>
@@ -408,25 +188,26 @@ const RichTextEditor: ComponentType<RichTextEditorProps> = ({
408
188
  <StyledTextInputContainer onLayout={onLayout}>
409
189
  <StyledBorderBackDrop themeState={state} themeFocused={isFocused} />
410
190
 
411
- <StyledTextInputAndLabelContainer
412
- style={{
413
- height: webviewHeight,
414
- }}
415
- testID="webViewWrapper"
416
- >
417
- <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
418
- <StyledWebView
419
- ref={webview}
420
- testID="webview"
421
- style={style}
422
- originWhitelist={['*']}
423
- source={{ html }}
424
- onMessage={onMessage}
425
- scrollEnabled={false}
426
- hideKeyboardAccessoryView
427
- keyboardDisplayRequiresUserAction={false}
428
- />
429
- </TouchableWithoutFeedback>
191
+ <StyledTextInputAndLabelContainer testID="webViewWrapper">
192
+ <BaseRichTextEditor
193
+ name={name}
194
+ value={value}
195
+ style={[
196
+ style,
197
+ {
198
+ marginHorizontal:
199
+ theme.__hd__.textInput.space.inputHorizontalMargin,
200
+ },
201
+ ]}
202
+ testID="webview"
203
+ onChange={onChange}
204
+ autoFocus={autoFocus}
205
+ editorRef={forwardedRef}
206
+ placeholder={placeholder}
207
+ onBlur={handleEditorBlur}
208
+ onFocus={handleEditorFocus}
209
+ onCursorChange={onCursorChange}
210
+ />
430
211
  </StyledTextInputAndLabelContainer>
431
212
  </StyledTextInputContainer>
432
213
  <StyledErrorAndHelpTextContainer>
@@ -11,5 +11,4 @@ export const StyledWebView = styled(WebView)(({ theme }) => ({
11
11
  backgroundColor: 'transparent',
12
12
  textAlignVertical: 'center',
13
13
  fontSize: theme.__hd__.textInput.fontSizes.text,
14
- marginHorizontal: theme.__hd__.textInput.space.inputHorizontalMargin,
15
14
  }));
@@ -10,6 +10,7 @@ import RichTextEditor from '../RichTextEditor';
10
10
  import renderWithTheme from '../../../testHelpers/renderWithTheme';
11
11
  import { theme } from '../../../index';
12
12
  import HeroDesignProvider from '../../HeroDesignProvider';
13
+ import type { RichTextEditorRef } from '../BaseRichTextEditor';
13
14
 
14
15
  type OnMessageCallback = (event: {
15
16
  nativeEvent: { data: string };
@@ -58,7 +59,6 @@ jest.mock('../utils/rnWebView', () => {
58
59
  /* eslint-disable */
59
60
  /// @ts-ignore
60
61
  import { postMessageMock } from '../utils/rnWebView';
61
- import { RichTextEditorRef } from '../../../../types';
62
62
  /* eslint-enable */
63
63
 
64
64
  describe('RichTextEditor', () => {
@@ -100,7 +100,7 @@ describe('RichTextEditor', () => {
100
100
  );
101
101
  });
102
102
 
103
- describe('recevied event editor-focus', () => {
103
+ describe('received event editor-focus', () => {
104
104
  it('should emit correct data', () => {
105
105
  const emittedEvents: string[] = [];
106
106
  Events.on(EditorEventEmitter, 'rich-text-editor/editor-focus', () => {
@@ -118,7 +118,7 @@ describe('RichTextEditor', () => {
118
118
  });
119
119
  });
120
120
 
121
- describe('recevied event editor-blur', () => {
121
+ describe('received event editor-blur', () => {
122
122
  it('should emit correct data', () => {
123
123
  const emittedEvents: string[] = [];
124
124
  Events.on(EditorEventEmitter, 'rich-text-editor/editor-blur', () => {
@@ -133,7 +133,7 @@ describe('RichTextEditor', () => {
133
133
  });
134
134
  });
135
135
 
136
- describe('recevied event mention-search', () => {
136
+ describe('received event mention-search', () => {
137
137
  it('should emit correct data', () => {
138
138
  const emittedEvents: string[] = [];
139
139
  Events.on(EditorEventEmitter, 'rich-text-editor/mention-search', () => {
@@ -148,7 +148,7 @@ describe('RichTextEditor', () => {
148
148
  });
149
149
  });
150
150
 
151
- describe('recevied event editor-change', () => {
151
+ describe('received event editor-change', () => {
152
152
  beforeEach(() => {
153
153
  onChangeMock.mockReset();
154
154
  });
@@ -163,7 +163,7 @@ describe('RichTextEditor', () => {
163
163
  });
164
164
  });
165
165
 
166
- describe('recevied event cursor-change', () => {
166
+ describe('received event cursor-change', () => {
167
167
  beforeEach(() => {
168
168
  onCursorChangeMock.mockReset();
169
169
  });
@@ -178,7 +178,7 @@ describe('RichTextEditor', () => {
178
178
  });
179
179
  });
180
180
 
181
- describe('recevied event editor-layout', () => {
181
+ describe('received event editor-layout', () => {
182
182
  it('should update height', () => {
183
183
  act(() => {
184
184
  onMessageOfLatestRendering({
@@ -201,7 +201,7 @@ describe('RichTextEditor', () => {
201
201
  );
202
202
 
203
203
  expect(wrapper.toJSON()).toMatchSnapshot();
204
- expect(wrapper.getByTestId('webViewWrapper')).toHaveStyle({
204
+ expect(wrapper.getByTestId('webview')).toHaveStyle({
205
205
  height: 480,
206
206
  });
207
207
  });
@@ -1,6 +1,6 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`RichTextEditor onMessage recevied event editor-layout should update height 1`] = `
3
+ exports[`RichTextEditor onMessage received event editor-layout should update height 1`] = `
4
4
  <View
5
5
  style={
6
6
  {
@@ -132,9 +132,7 @@ exports[`RichTextEditor onMessage recevied event editor-layout should update hei
132
132
  "flexGrow": 2,
133
133
  "flexShrink": 1,
134
134
  },
135
- {
136
- "height": 480,
137
- },
135
+ undefined,
138
136
  ]
139
137
  }
140
138
  testID="webViewWrapper"
@@ -206,12 +204,13 @@ exports[`RichTextEditor onMessage recevied event editor-layout should update hei
206
204
  {
207
205
  "backgroundColor": "transparent",
208
206
  "fontSize": 16,
209
- "marginHorizontal": 8,
210
207
  "minHeight": 24,
211
208
  "textAlignVertical": "center",
212
209
  },
213
210
  {
214
211
  "backgroundColor": "yellow",
212
+ "height": 480,
213
+ "marginHorizontal": 8,
215
214
  },
216
215
  ]
217
216
  }
@@ -458,9 +457,7 @@ exports[`RichTextEditor should render correctly 1`] = `
458
457
  "flexGrow": 2,
459
458
  "flexShrink": 1,
460
459
  },
461
- {
462
- "height": undefined,
463
- },
460
+ undefined,
464
461
  ]
465
462
  }
466
463
  testID="webViewWrapper"
@@ -532,12 +529,13 @@ exports[`RichTextEditor should render correctly 1`] = `
532
529
  {
533
530
  "backgroundColor": "transparent",
534
531
  "fontSize": 16,
535
- "marginHorizontal": 8,
536
532
  "minHeight": 24,
537
533
  "textAlignVertical": "center",
538
534
  },
539
535
  {
540
536
  "backgroundColor": "yellow",
537
+ "height": undefined,
538
+ "marginHorizontal": 8,
541
539
  },
542
540
  ]
543
541
  }
@@ -7,3 +7,8 @@ export enum ToolbarEvents {
7
7
  HeadingOne = 'heading-one',
8
8
  HeadingTwo = 'heading-two',
9
9
  }
10
+
11
+ export enum EditorEvents {
12
+ EditorFocus = 'editor-focus',
13
+ EditorBlur = 'editor-blur',
14
+ }
@@ -0,0 +1,60 @@
1
+ import { useCallback } from 'react';
2
+
3
+ import * as Events from '../utils/events';
4
+ import { emitter } from '../EditorEvent';
5
+ import type { EditorEvents, ToolbarEvents } from '../constants';
6
+
7
+ type SubscribableEvent = ToolbarEvents | EditorEvents;
8
+
9
+ /**
10
+ * Hook to subscribe to events for a rich text editor
11
+ * @param editorName - The name of the editor to subscribe to events for
12
+ * @returns An object with two functions: emitEvent and subscribeToEvents
13
+ * @example
14
+ * const { emitEvent, subscribeToEvents } = useRichTextEditorEvents('editorName');
15
+ * subscribeToEvents(EditorEvents.EditorFocus, () => {
16
+ * console.log('Editor focused');
17
+ * });
18
+ * emitEvent({ type: EditorEvents.EditorFocus, data: null });
19
+ */
20
+ const useRichTextEditorEvents = (editorName: string) => {
21
+ const normalizeEventName = useCallback(
22
+ (event: string) => `${editorName}/${event}`,
23
+ [editorName]
24
+ );
25
+
26
+ const subscribeToEvents = useCallback(
27
+ <TEventName extends SubscribableEvent>(
28
+ eventName: TEventName,
29
+ onEvent: (data: unknown) => void
30
+ ) =>
31
+ Events.on(
32
+ emitter,
33
+ normalizeEventName(eventName),
34
+ (eventData: unknown) => {
35
+ onEvent(eventData);
36
+ }
37
+ ),
38
+ [normalizeEventName]
39
+ );
40
+
41
+ const emitEvent = useCallback(
42
+ <TEventName extends ToolbarEvents>({
43
+ type,
44
+ data,
45
+ }: {
46
+ type: TEventName;
47
+ data: unknown;
48
+ }) => {
49
+ Events.emit(emitter, normalizeEventName(type), data);
50
+ },
51
+ [normalizeEventName]
52
+ );
53
+
54
+ return {
55
+ emitEvent,
56
+ subscribeToEvents,
57
+ };
58
+ };
59
+
60
+ export default useRichTextEditorEvents;
@@ -1,11 +1,19 @@
1
1
  import Toolbar from './EditorToolbar';
2
2
  import MentionList from './MentionList';
3
3
  import RichTextEditor from './RichTextEditor';
4
- import type { RichTextEditorProps, RichTextEditorRef } from './RichTextEditor';
4
+ import { EditorEvents, ToolbarEvents } from './constants';
5
+ import useRichTextEditorEvents from './hooks/useRichTextEditorEvents';
6
+ import type { RichTextEditorProps } from './RichTextEditor';
7
+ import BaseRichTextEditor from './BaseRichTextEditor';
8
+ import type { RichTextEditorRef } from './BaseRichTextEditor';
5
9
 
6
10
  export type { RichTextEditorProps, RichTextEditorRef };
7
11
 
8
12
  export default Object.assign(RichTextEditor, {
9
- MentionList,
10
13
  Toolbar,
14
+ MentionList,
15
+ EditorEvents,
16
+ ToolbarEvents,
17
+ useRichTextEditorEvents,
18
+ Base: BaseRichTextEditor,
11
19
  });