react-native-enriched-markdown-scaffold 1.0.2-scaffold.4 → 1.0.2-scaffold.6

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 (71) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +15 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +15 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +4 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +107 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +19 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +22 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +48 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +22 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +42 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +14 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +230 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -1
  18. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +8 -1
  19. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +8 -1
  20. package/docs/API_REFERENCE.md +68 -0
  21. package/docs/WEB.md +1 -0
  22. package/ios/EnrichedMarkdown.mm +188 -2
  23. package/ios/EnrichedMarkdownText.mm +117 -1
  24. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
  25. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
  26. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  27. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +4 -0
  28. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  29. package/ios/utils/ENRMLinkChip.m +38 -20
  30. package/ios/utils/ENRMLinkLayout.m +12 -12
  31. package/ios/utils/ENRMSelectionChange.h +96 -0
  32. package/ios/utils/ENRMSelectionChange.m +204 -0
  33. package/ios/utils/EditMenuUtils+macOS.m +1 -1
  34. package/ios/utils/EditMenuUtils.h +3 -0
  35. package/ios/utils/EditMenuUtils.m +116 -0
  36. package/ios/utils/MarkdownAccessibilityElementBuilder.m +1 -2
  37. package/ios/views/ENRMCodeBlockContainerView.h +3 -0
  38. package/ios/views/ENRMCodeBlockContainerView.m +3 -0
  39. package/ios/views/ENRMMathContainerView.h +3 -0
  40. package/ios/views/ENRMMathContainerView.m +3 -0
  41. package/ios/views/TableContainerView.h +3 -0
  42. package/ios/views/TableContainerView.m +4 -1
  43. package/lib/module/EnrichedMarkdownNativeComponent.ts +70 -1
  44. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +70 -1
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/jest/index.js +14 -0
  47. package/lib/module/jest/index.js.map +1 -1
  48. package/lib/module/native/EnrichedMarkdownText.js +46 -4
  49. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  50. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +63 -2
  51. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  52. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +63 -2
  53. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  54. package/lib/typescript/src/index.d.ts +2 -2
  55. package/lib/typescript/src/index.d.ts.map +1 -1
  56. package/lib/typescript/src/jest/index.d.ts +1 -1
  57. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  58. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  59. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  60. package/lib/typescript/src/types/MarkdownTextProps.d.ts +46 -3
  61. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  62. package/lib/typescript/src/types/events.d.ts +36 -0
  63. package/lib/typescript/src/types/events.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/EnrichedMarkdownNativeComponent.ts +70 -1
  66. package/src/EnrichedMarkdownTextNativeComponent.ts +70 -1
  67. package/src/index.tsx +3 -0
  68. package/src/jest/index.tsx +22 -1
  69. package/src/native/EnrichedMarkdownText.tsx +87 -9
  70. package/src/types/MarkdownTextProps.ts +53 -2
  71. package/src/types/events.ts +38 -0
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  codegenNativeComponent,
3
+ codegenNativeCommands,
3
4
  type ViewProps,
4
5
  type CodegenTypes,
5
6
  type ColorValue,
7
+ type HostComponent,
6
8
  } from 'react-native';
9
+ import type React from 'react';
7
10
 
8
11
  // All block styles extend this interface
9
12
  interface BaseBlockStyleInternal {
@@ -274,6 +277,39 @@ export interface LinkLayoutEvent {
274
277
  }[];
275
278
  }
276
279
 
280
+ /**
281
+ * Current text selection, or a long-pressed table / math / code block while
282
+ * `contextMenuHidden` is on. `active: false` means the selection was cleared
283
+ * (every other field is then empty / zero). Rects are in dp / pt, relative to
284
+ * the native view's origin; rect objects are written inline because codegen
285
+ * only accepts inline object literals in event payloads.
286
+ */
287
+ export interface SelectionChangeEvent {
288
+ active: boolean;
289
+ source: string;
290
+ text: string;
291
+ markdown: string;
292
+ whole: boolean;
293
+ rect: {
294
+ x: CodegenTypes.Double;
295
+ y: CodegenTypes.Double;
296
+ width: CodegenTypes.Double;
297
+ height: CodegenTypes.Double;
298
+ };
299
+ startRect: {
300
+ x: CodegenTypes.Double;
301
+ y: CodegenTypes.Double;
302
+ width: CodegenTypes.Double;
303
+ height: CodegenTypes.Double;
304
+ };
305
+ endRect: {
306
+ x: CodegenTypes.Double;
307
+ y: CodegenTypes.Double;
308
+ width: CodegenTypes.Double;
309
+ height: CodegenTypes.Double;
310
+ };
311
+ }
312
+
277
313
  export interface TaskListItemPressEvent {
278
314
  index: CodegenTypes.Int32;
279
315
  checked: boolean;
@@ -441,6 +477,18 @@ export interface NativeProps extends ViewProps {
441
477
  * Deduplicated: not re-fired when the payload is unchanged.
442
478
  */
443
479
  onLinkLayout?: CodegenTypes.DirectEventHandler<LinkLayoutEvent>;
480
+ /**
481
+ * When true, the view reports selection changes (and, with
482
+ * `contextMenuHidden`, block long-presses) through `onSelectionChange`.
483
+ * Set by the JS wrapper only when `onSelectionChange` is provided.
484
+ * @default false
485
+ */
486
+ emitSelectionChange?: CodegenTypes.WithDefault<boolean, false>;
487
+ /**
488
+ * Fired while `emitSelectionChange` is on: coalesced to one event per frame,
489
+ * deduplicated, and `active: false` once the selection is gone.
490
+ */
491
+ onSelectionChange?: CodegenTypes.DirectEventHandler<SelectionChangeEvent>;
444
492
  /**
445
493
  * Callback fired when a task list checkbox is tapped.
446
494
  * Receives the 0-based task index, current checked state, and the item's plain text.
@@ -480,6 +528,16 @@ export interface NativeProps extends ViewProps {
480
528
  * @default true
481
529
  */
482
530
  enableLinkPreview?: CodegenTypes.WithDefault<boolean, true>;
531
+ /**
532
+ * When `true`, long-pressing a link selects its label and shows the
533
+ * selection edit menu instead of the system link preview (iOS only).
534
+ * Takes precedence over `enableLinkPreview` and `onLinkLongPress`.
535
+ *
536
+ * Android: No-op.
537
+ *
538
+ * @default false
539
+ */
540
+ linkLongPressSelects?: CodegenTypes.WithDefault<boolean, false>;
483
541
  /**
484
542
  * - iOS: Controls text selection and link previews on long press.
485
543
  * - Android: Controls text selection.
@@ -594,6 +652,17 @@ export interface NativeProps extends ViewProps {
594
652
  writingDirection?: CodegenTypes.WithDefault<string, 'first-strong'>;
595
653
  }
596
654
 
655
+ type ComponentType = HostComponent<NativeProps>;
656
+
657
+ interface NativeCommands {
658
+ /** Clears the text selection (and any active block) and fires `onSelectionChange` with `active: false`. */
659
+ clearSelection: (viewRef: React.ElementRef<ComponentType>) => void;
660
+ }
661
+
662
+ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
663
+ supportedCommands: ['clearSelection'],
664
+ });
665
+
597
666
  export default codegenNativeComponent<NativeProps>('EnrichedMarkdown', {
598
667
  interfaceOnly: true,
599
- });
668
+ }) as HostComponent<NativeProps>;
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  codegenNativeComponent,
3
+ codegenNativeCommands,
3
4
  type ViewProps,
4
5
  type CodegenTypes,
5
6
  type ColorValue,
7
+ type HostComponent,
6
8
  } from 'react-native';
9
+ import type React from 'react';
7
10
 
8
11
  // All block styles extend this interface
9
12
  interface BaseBlockStyleInternal {
@@ -275,6 +278,39 @@ export interface LinkLayoutEvent {
275
278
  }[];
276
279
  }
277
280
 
281
+ /**
282
+ * Current text selection, or a long-pressed table / math / code block while
283
+ * `contextMenuHidden` is on. `active: false` means the selection was cleared
284
+ * (every other field is then empty / zero). Rects are in dp / pt, relative to
285
+ * the native view's origin; rect objects are written inline because codegen
286
+ * only accepts inline object literals in event payloads.
287
+ */
288
+ export interface SelectionChangeEvent {
289
+ active: boolean;
290
+ source: string;
291
+ text: string;
292
+ markdown: string;
293
+ whole: boolean;
294
+ rect: {
295
+ x: CodegenTypes.Double;
296
+ y: CodegenTypes.Double;
297
+ width: CodegenTypes.Double;
298
+ height: CodegenTypes.Double;
299
+ };
300
+ startRect: {
301
+ x: CodegenTypes.Double;
302
+ y: CodegenTypes.Double;
303
+ width: CodegenTypes.Double;
304
+ height: CodegenTypes.Double;
305
+ };
306
+ endRect: {
307
+ x: CodegenTypes.Double;
308
+ y: CodegenTypes.Double;
309
+ width: CodegenTypes.Double;
310
+ height: CodegenTypes.Double;
311
+ };
312
+ }
313
+
278
314
  export interface TaskListItemPressEvent {
279
315
  index: CodegenTypes.Int32;
280
316
  checked: boolean;
@@ -442,6 +478,18 @@ export interface NativeProps extends ViewProps {
442
478
  * Deduplicated: not re-fired when the payload is unchanged.
443
479
  */
444
480
  onLinkLayout?: CodegenTypes.DirectEventHandler<LinkLayoutEvent>;
481
+ /**
482
+ * When true, the view reports selection changes (and, with
483
+ * `contextMenuHidden`, block long-presses) through `onSelectionChange`.
484
+ * Set by the JS wrapper only when `onSelectionChange` is provided.
485
+ * @default false
486
+ */
487
+ emitSelectionChange?: CodegenTypes.WithDefault<boolean, false>;
488
+ /**
489
+ * Fired while `emitSelectionChange` is on: coalesced to one event per frame,
490
+ * deduplicated, and `active: false` once the selection is gone.
491
+ */
492
+ onSelectionChange?: CodegenTypes.DirectEventHandler<SelectionChangeEvent>;
445
493
  /**
446
494
  * Callback fired when a task list checkbox is tapped.
447
495
  * Receives the 0-based task index, current checked state, and the item's plain text.
@@ -481,6 +529,16 @@ export interface NativeProps extends ViewProps {
481
529
  * @default true
482
530
  */
483
531
  enableLinkPreview?: CodegenTypes.WithDefault<boolean, true>;
532
+ /**
533
+ * When `true`, long-pressing a link selects its label and shows the
534
+ * selection edit menu instead of the system link preview (iOS only).
535
+ * Takes precedence over `enableLinkPreview` and `onLinkLongPress`.
536
+ *
537
+ * Android: No-op.
538
+ *
539
+ * @default false
540
+ */
541
+ linkLongPressSelects?: CodegenTypes.WithDefault<boolean, false>;
484
542
  /**
485
543
  * - iOS: Controls text selection and link previews on long press.
486
544
  * - Android: Controls text selection.
@@ -594,6 +652,17 @@ export interface NativeProps extends ViewProps {
594
652
  writingDirection?: CodegenTypes.WithDefault<string, 'first-strong'>;
595
653
  }
596
654
 
655
+ type ComponentType = HostComponent<NativeProps>;
656
+
657
+ interface NativeCommands {
658
+ /** Clears the text selection (and any active block) and fires `onSelectionChange` with `active: false`. */
659
+ clearSelection: (viewRef: React.ElementRef<ComponentType>) => void;
660
+ }
661
+
662
+ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
663
+ supportedCommands: ['clearSelection'],
664
+ });
665
+
597
666
  export default codegenNativeComponent<NativeProps>('EnrichedMarkdownText', {
598
667
  interfaceOnly: true,
599
- });
668
+ }) as HostComponent<NativeProps>;
package/src/index.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  export { default as EnrichedMarkdownText } from './native/EnrichedMarkdownText';
2
2
  export type {
3
3
  EnrichedMarkdownTextProps,
4
+ EnrichedMarkdownTextInstance,
4
5
  StreamingConfig,
5
6
  MarkdownStyle,
6
7
  Md4cFlags,
@@ -17,6 +18,8 @@ export type {
17
18
  CopyPressEvent,
18
19
  CopyEvent,
19
20
  OnPasteEvent,
21
+ SelectionChangeEvent,
22
+ SelectionRect,
20
23
  } from './types/events';
21
24
  export type {
22
25
  AccessibilityLabels,
@@ -26,7 +26,10 @@ import type {
26
26
  EnrichedMarkdownTextInputProps,
27
27
  InsertMathOptions,
28
28
  } from '../EnrichedMarkdownTextInput';
29
- import type { EnrichedMarkdownTextProps } from '../native/EnrichedMarkdownText';
29
+ import type {
30
+ EnrichedMarkdownTextInstance,
31
+ EnrichedMarkdownTextProps,
32
+ } from '../native/EnrichedMarkdownText';
30
33
 
31
34
  export {
32
35
  DEFAULT_ACCESSIBILITY_LABELS,
@@ -165,6 +168,7 @@ export const EnrichedMarkdownTextInput = ({
165
168
  };
166
169
 
167
170
  export const EnrichedMarkdownText = ({
171
+ ref,
168
172
  markdown,
169
173
  containerStyle,
170
174
  testID,
@@ -178,7 +182,9 @@ export const EnrichedMarkdownText = ({
178
182
  onLinkPress: _onLinkPress,
179
183
  onLinkLongPress: _onLinkLongPress,
180
184
  onTaskListItemPress: _onTaskListItemPress,
185
+ onSelectionChange: _onSelectionChange,
181
186
  enableLinkPreview: _enableLinkPreview,
187
+ linkLongPressSelects: _linkLongPressSelects,
182
188
  selectable: _selectable,
183
189
  md4cFlags: _md4cFlags,
184
190
  allowFontScaling: _allowFontScaling,
@@ -198,6 +204,21 @@ export const EnrichedMarkdownText = ({
198
204
  lineBreakStrategyIOS: _lineBreakStrategyIOS,
199
205
  writingDirection: _writingDirection,
200
206
  }: EnrichedMarkdownTextProps) => {
207
+ const instanceRef = useRef<EnrichedMarkdownTextInstance | null>(null);
208
+ if (instanceRef.current === null) {
209
+ instanceRef.current = {
210
+ // No native selection under Jest; the mock records the call.
211
+ clearSelection: spy(() => {}),
212
+ measure: spy((callback) => callback(0, 0, 0, 0, 0, 0)),
213
+ measureInWindow: spy((callback) => callback(0, 0, 0, 0)),
214
+ measureLayout: spy((_relativeToNativeNode, onSuccess) =>
215
+ onSuccess(0, 0, 0, 0)
216
+ ),
217
+ };
218
+ }
219
+
220
+ useImperativeHandle(ref, () => instanceRef.current!);
221
+
201
222
  return (
202
223
  <Text
203
224
  style={containerStyle}
@@ -1,10 +1,22 @@
1
- import { useMemo, useCallback, useRef, useEffect, useState } from 'react';
2
- import EnrichedMarkdownTextNativeComponent from '../EnrichedMarkdownTextNativeComponent';
1
+ import {
2
+ useMemo,
3
+ useCallback,
4
+ useRef,
5
+ useEffect,
6
+ useState,
7
+ useImperativeHandle,
8
+ } from 'react';
9
+ import EnrichedMarkdownTextNativeComponent, {
10
+ Commands as TextCommands,
11
+ } from '../EnrichedMarkdownTextNativeComponent';
3
12
  import type {
4
13
  LinkLayoutEvent,
5
14
  MarkdownStyleInternal,
15
+ SelectionChangeEvent as NativeSelectionChangeEvent,
6
16
  } from '../EnrichedMarkdownTextNativeComponent';
7
- import EnrichedMarkdownNativeComponent from '../EnrichedMarkdownNativeComponent';
17
+ import EnrichedMarkdownNativeComponent, {
18
+ Commands as MarkdownCommands,
19
+ } from '../EnrichedMarkdownNativeComponent';
8
20
  import { normalizeMarkdownStyle } from '../normalizeMarkdownStyle';
9
21
  import { resolveAccessibilityLabels } from '../accessibilityLabelDefaults';
10
22
  import {
@@ -12,10 +24,15 @@ import {
12
24
  normalizeLegacyBooleanMenuItem,
13
25
  } from '../normalizeMenuItem';
14
26
  import { StyleSheet, View } from 'react-native';
15
- import type { LayoutChangeEvent, NativeSyntheticEvent } from 'react-native';
27
+ import type {
28
+ HostInstance,
29
+ LayoutChangeEvent,
30
+ NativeSyntheticEvent,
31
+ } from 'react-native';
16
32
  import type { MarkdownStyle, Md4cFlags } from '../types/MarkdownStyle';
17
33
  import type {
18
34
  EnrichedMarkdownTextProps,
35
+ EnrichedMarkdownTextInstance,
19
36
  StreamingConfig,
20
37
  ContextMenuItem,
21
38
  SelectionMenuConfig,
@@ -30,11 +47,14 @@ import type {
30
47
  CopyPressEvent,
31
48
  CopyEvent,
32
49
  OnContextMenuItemPressEvent,
50
+ SelectionChangeEvent,
51
+ SelectionRect,
33
52
  } from '../types/events';
34
53
 
35
54
  export type { MarkdownStyle, Md4cFlags };
36
55
  export type {
37
56
  EnrichedMarkdownTextProps,
57
+ EnrichedMarkdownTextInstance,
38
58
  StreamingConfig,
39
59
  ContextMenuItem,
40
60
  SelectionMenuConfig,
@@ -48,6 +68,8 @@ export type {
48
68
  TaskListItemPressEvent,
49
69
  CopyPressEvent,
50
70
  CopyEvent,
71
+ SelectionChangeEvent,
72
+ SelectionRect,
51
73
  };
52
74
 
53
75
  // Default English labels for the built-in selection menu actions. Defaults are
@@ -149,6 +171,7 @@ const defaultMd4cFlags: Md4cFlags = {
149
171
  };
150
172
 
151
173
  export const EnrichedMarkdownText = ({
174
+ ref,
152
175
  markdown,
153
176
  markdownStyle = {},
154
177
  containerStyle,
@@ -158,7 +181,9 @@ export const EnrichedMarkdownText = ({
158
181
  enableTaskListItemToggle = true,
159
182
  onCopyPress,
160
183
  onCopy,
184
+ onSelectionChange,
161
185
  enableLinkPreview,
186
+ linkLongPressSelects = false,
162
187
  selectable = true,
163
188
  md4cFlags = defaultMd4cFlags,
164
189
  allowFontScaling = true,
@@ -315,6 +340,54 @@ export const EnrichedMarkdownText = ({
315
340
  setNativeOffset((prev) => (prev.x === x && prev.y === y ? prev : { x, y }));
316
341
  }, []);
317
342
 
343
+ // Rects stay relative to the native view — the view the ref's
344
+ // `measureInWindow` measures — even when decorations wrap it.
345
+ const handleSelectionChange = useCallback(
346
+ // Codegen types `source` as a plain string; native only sends the four
347
+ // values of SelectionChangeEvent['source'].
348
+ (e: NativeSyntheticEvent<NativeSelectionChangeEvent>) => {
349
+ const { source, ...payload } = e.nativeEvent;
350
+ onSelectionChange?.({
351
+ ...payload,
352
+ source: source as SelectionChangeEvent['source'],
353
+ });
354
+ },
355
+ [onSelectionChange]
356
+ );
357
+
358
+ const nativeRef = useRef<HostInstance | null>(null);
359
+ const isGithubFlavor = flavor === 'github';
360
+ useImperativeHandle(
361
+ ref,
362
+ () => ({
363
+ clearSelection: () => {
364
+ const node = nativeRef.current;
365
+ if (node == null) return;
366
+ // Codegen's ViewRef resolves to `never` with RN 0.84's function-based
367
+ // HostComponent type — the cast is safe at runtime.
368
+ if (isGithubFlavor) {
369
+ MarkdownCommands.clearSelection(
370
+ node as Parameters<(typeof MarkdownCommands)['clearSelection']>[0]
371
+ );
372
+ } else {
373
+ TextCommands.clearSelection(
374
+ node as Parameters<(typeof TextCommands)['clearSelection']>[0]
375
+ );
376
+ }
377
+ },
378
+ measure: (callback) => nativeRef.current?.measure(callback),
379
+ measureInWindow: (callback) =>
380
+ nativeRef.current?.measureInWindow(callback),
381
+ measureLayout: (relativeToNativeNode, onSuccess, onFail) =>
382
+ nativeRef.current?.measureLayout(
383
+ relativeToNativeNode,
384
+ onSuccess,
385
+ onFail
386
+ ),
387
+ }),
388
+ [isGithubFlavor]
389
+ );
390
+
318
391
  const tableMode = streamingConfig?.tableMode ?? 'progressive';
319
392
  const codeBlockMode = streamingConfig?.codeBlockMode ?? 'progressive';
320
393
  const normalizedStreamingConfig = useMemo(
@@ -396,7 +469,10 @@ export const EnrichedMarkdownText = ({
396
469
  enableTaskListItemToggle,
397
470
  onCopyPress: handleCopyPress,
398
471
  onCopyRequest: handleCopyRequest,
472
+ emitSelectionChange: onSelectionChange != null,
473
+ onSelectionChange: handleSelectionChange,
399
474
  enableLinkPreview: onLinkLongPress == null && (enableLinkPreview ?? true),
475
+ linkLongPressSelects,
400
476
  selectable,
401
477
  md4cFlags: normalizedMd4cFlags,
402
478
  allowFontScaling,
@@ -420,21 +496,23 @@ export const EnrichedMarkdownText = ({
420
496
  ...rest,
421
497
  };
422
498
 
423
- const NativeComponent =
424
- flavor === 'github'
425
- ? EnrichedMarkdownNativeComponent
426
- : EnrichedMarkdownTextNativeComponent;
499
+ const NativeComponent = isGithubFlavor
500
+ ? EnrichedMarkdownNativeComponent
501
+ : EnrichedMarkdownTextNativeComponent;
427
502
 
428
503
  // Without decorations the native view is returned as-is (no wrapper, no
429
504
  // layout events) so existing layouts are untouched.
430
505
  if (!decorated) {
431
- return <NativeComponent {...sharedProps} onLayout={onLayout} />;
506
+ return (
507
+ <NativeComponent {...sharedProps} ref={nativeRef} onLayout={onLayout} />
508
+ );
432
509
  }
433
510
 
434
511
  return (
435
512
  <View style={containerStyle} onLayout={onLayout}>
436
513
  <NativeComponent
437
514
  {...sharedProps}
515
+ ref={nativeRef}
438
516
  style={undefined}
439
517
  emitLinkLayout
440
518
  onLinkLayout={handleLinkLayout}
@@ -1,5 +1,11 @@
1
- import type { ReactNode } from 'react';
2
- import type { ColorValue, ViewProps, ViewStyle, TextStyle } from 'react-native';
1
+ import type { ReactNode, RefObject } from 'react';
2
+ import type {
3
+ ColorValue,
4
+ HostInstance,
5
+ ViewProps,
6
+ ViewStyle,
7
+ TextStyle,
8
+ } from 'react-native';
3
9
  import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle';
4
10
  import type { AccessibilityLabels } from './AccessibilityLabels';
5
11
  import type {
@@ -8,8 +14,22 @@ import type {
8
14
  TaskListItemPressEvent,
9
15
  CopyPressEvent,
10
16
  CopyEvent,
17
+ SelectionChangeEvent,
11
18
  } from './events';
12
19
 
20
+ /** Imperative handle of `EnrichedMarkdownText`, reached through its `ref`. */
21
+ export interface EnrichedMarkdownTextInstance {
22
+ /**
23
+ * Clears the text selection (and any long-pressed block) and fires
24
+ * `onSelectionChange` with `active: false`.
25
+ * @platform ios, android
26
+ */
27
+ clearSelection: () => void;
28
+ measure: HostInstance['measure'];
29
+ measureInWindow: HostInstance['measureInWindow'];
30
+ measureLayout: HostInstance['measureLayout'];
31
+ }
32
+
13
33
  /**
14
34
  * A rect in dp, relative to the component's content box (inside any
15
35
  * `containerStyle` padding).
@@ -169,6 +189,11 @@ export interface StreamingConfig {
169
189
  }
170
190
 
171
191
  export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
192
+ /**
193
+ * Imperative handle: `clearSelection()` plus the host `measure*` methods.
194
+ * @platform ios, android
195
+ */
196
+ ref?: RefObject<EnrichedMarkdownTextInstance | null>;
172
197
  /**
173
198
  * Markdown content to render.
174
199
  * @platform ios, android, web
@@ -269,6 +294,22 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
269
294
  * @platform ios, android
270
295
  */
271
296
  onCopy?: (event: CopyEvent) => void;
297
+ /**
298
+ * Reports the text selection so the app can draw its own menu (pair it with
299
+ * `contextMenuHidden`). Fires at most once per frame while the selection
300
+ * changes, and with `active: false` once it is gone. With
301
+ * `contextMenuHidden`, a long press on a table / math / code block fires it
302
+ * too (`source` is the block kind, `whole: true`), since those blocks have no
303
+ * text selection of their own. Only one part of a component holds a
304
+ * selection at a time; clearing the selection of *other* components is up to
305
+ * the app (`ref.current.clearSelection()`).
306
+ *
307
+ * Rects are relative to the view that `ref.current.measureInWindow`
308
+ * measures — add that origin to position UI on screen. There is no
309
+ * "dragging" flag: debounce before showing a menu.
310
+ * @platform ios, android
311
+ */
312
+ onSelectionChange?: (event: SelectionChangeEvent) => void;
272
313
  /**
273
314
  * Controls whether the system link preview is shown on long press (iOS only).
274
315
  *
@@ -282,6 +323,16 @@ export interface EnrichedMarkdownTextProps extends Omit<ViewProps, 'style'> {
282
323
  * @platform ios
283
324
  */
284
325
  enableLinkPreview?: boolean;
326
+ /**
327
+ * When `true`, long-pressing a link selects its label and shows the
328
+ * selection edit menu, like a manual text selection, instead of the system
329
+ * link preview. Takes precedence over `enableLinkPreview` and
330
+ * `onLinkLongPress`.
331
+ *
332
+ * @default false
333
+ * @platform ios
334
+ */
335
+ linkLongPressSelects?: boolean;
285
336
  /**
286
337
  * Controls text selection.
287
338
  * - iOS: Controls text selection and link previews on long press.
@@ -40,6 +40,44 @@ export interface CopyEvent {
40
40
  whole: boolean;
41
41
  }
42
42
 
43
+ /**
44
+ * A rect in dp, relative to the component's native view: add the origin from
45
+ * the component ref's `measureInWindow` to get window coordinates.
46
+ */
47
+ export interface SelectionRect {
48
+ x: number;
49
+ y: number;
50
+ width: number;
51
+ height: number;
52
+ }
53
+
54
+ /**
55
+ * Payload of `onSelectionChange`: the current text selection, or a table /
56
+ * math / code block long-pressed while `contextMenuHidden` is on. Content
57
+ * fields carry what `onCopy` would receive for the same selection.
58
+ */
59
+ export interface SelectionChangeEvent {
60
+ /** `false` once the selection is gone; every other field is then empty / zero. */
61
+ active: boolean;
62
+ /** `'selection'` for text; the block kind for a long-pressed block. */
63
+ source: CopyEvent['source'];
64
+ /** Rendered text of the selection, or the block's raw content. */
65
+ text: string;
66
+ /** Same as `CopyEvent.markdown` for this selection. */
67
+ markdown: string;
68
+ /** Same as `CopyEvent.whole`. Always `true` for blocks. */
69
+ whole: boolean;
70
+ /**
71
+ * Bounding box of the selection (full content width when it spans several
72
+ * lines); the block's frame for blocks.
73
+ */
74
+ rect: SelectionRect;
75
+ /** Selected part of the first line (block: same as `rect`). */
76
+ startRect: SelectionRect;
77
+ /** Selected part of the last line (block: same as `rect`). */
78
+ endRect: SelectionRect;
79
+ }
80
+
43
81
  /**
44
82
  * Native-level context menu item config sent to the native component.
45
83
  * Does not include the `onPress` callback — callbacks are managed on the JS side.