From b643a8b1083b590b2115e9dfa617bbf8ec9b6617 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Thu, 18 Jun 2026 21:46:48 +0200 Subject: [PATCH 1/5] feat: add selectionMenuConfig and formatMenuConfig props to EnrichedMarkdownTextInput Co-authored-by: Cursor --- docs/API_REFERENCE.md | 33 ++++++++ docs/COPY_OPTIONS.md | 11 +++ .../input/EnrichedMarkdownTextInputManager.kt | 14 ++++ .../input/toolbar/InputContextMenu.kt | 18 ++++- .../EnrichedMarkdownTextInput+ContextMenu.mm | 81 +++++++++++-------- .../EnrichedMarkdownTextInput+Internal.h | 6 ++ .../ios/input/EnrichedMarkdownTextInput.mm | 27 +++++-- .../src/EnrichedMarkdownTextInput.tsx | 33 ++++++++ ...nrichedMarkdownTextInputNativeComponent.ts | 11 +++ .../src/index.tsx | 1 + 10 files changed, 188 insertions(+), 47 deletions(-) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index ce350ef2..bebb312b 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -689,6 +689,39 @@ interface ContextMenuItem { /> ``` +### `selectionMenuConfig` + +Controls built-in items in the text selection context menu. The Format submenu and the Copy as Markdown action can each be hidden independently. Custom app-provided actions are controlled separately with `contextMenuItems`. + +| Type | Default Value | Platform | +| -------------------------- | -------------------------------------- | ------------------- | +| `InputSelectionMenuConfig` | `{ format: true, copyAsMarkdown: true }` | iOS, Android, macOS | + +**`InputSelectionMenuConfig` shape:** + +```ts +interface InputSelectionMenuConfig { + /** Shows the built-in "Format" submenu (Bold, Italic, Underline, etc.). */ + format?: boolean; + /** Shows the built-in "Copy as Markdown" action. */ + copyAsMarkdown?: boolean; +} +``` + +**Example:** + +```tsx +// Hide both the Format submenu and the Copy as Markdown action + + +// Keep Format but hide Copy as Markdown + +``` + ### Ref Methods All methods are called imperatively on the ref (`ref.current?.methodName()`). diff --git a/docs/COPY_OPTIONS.md b/docs/COPY_OPTIONS.md index 6d4d944e..0ca0f71d 100644 --- a/docs/COPY_OPTIONS.md +++ b/docs/COPY_OPTIONS.md @@ -43,3 +43,14 @@ Use `selectionMenuConfig` to hide built-in selection menu actions while keeping }} /> ``` + +`EnrichedMarkdownTextInput` supports the same prop. In addition to `copyAsMarkdown`, the input's `selectionMenuConfig` can hide the built-in **Format** submenu: + +```tsx + +``` diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt index 257d7548..0352d177 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt @@ -31,6 +31,7 @@ import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent import com.swmansion.enriched.markdown.input.layout.InputMeasurementStore import com.swmansion.enriched.markdown.input.model.StyleType +import com.swmansion.enriched.markdown.input.toolbar.InputSelectionMenuConfig import com.swmansion.enriched.markdown.utils.input.BorderPropsApplicator import com.swmansion.enriched.markdown.utils.input.MarkdownStyleParser @@ -263,6 +264,19 @@ class EnrichedMarkdownTextInputManager : view.setContextMenuItems(items) } + @ReactProp(name = "selectionMenuConfig") + override fun setSelectionMenuConfig( + view: EnrichedMarkdownTextInputView?, + value: ReadableMap?, + ) { + if (view == null || value == null) return + view.contextMenu.selectionMenuConfig = + InputSelectionMenuConfig( + format = value.getBoolean("format"), + copyAsMarkdown = value.getBoolean("copyAsMarkdown"), + ) + } + @ReactProp(name = "linkRegex") override fun setLinkRegex( view: EnrichedMarkdownTextInputView?, diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt index 38c0b70d..f2ec225d 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt @@ -16,10 +16,16 @@ import com.swmansion.enriched.markdown.input.model.StyleType // TODO: Wrap all user-facing strings for localization support. +data class InputSelectionMenuConfig( + val format: Boolean = true, + val copyAsMarkdown: Boolean = true, +) + class InputContextMenu( private val view: EnrichedMarkdownTextInputView, ) { private var customItemTexts: List = emptyList() + var selectionMenuConfig: InputSelectionMenuConfig = InputSelectionMenuConfig() fun setContextMenuItems(items: List) { customItemTexts = items @@ -42,13 +48,17 @@ class InputContextMenu( menu.removeGroup(FORMAT_MENU_GROUP_ID) menu.removeGroup(CUSTOM_MENU_GROUP_ID) - val formatSubMenu = menu.addSubMenu(FORMAT_MENU_GROUP_ID, MENU_FORMAT_ID, 100, "Format") - FORMAT_ITEMS.forEachIndexed { index, (title, _) -> - formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, title) + if (selectionMenuConfig.format) { + val formatSubMenu = menu.addSubMenu(FORMAT_MENU_GROUP_ID, MENU_FORMAT_ID, 100, "Format") + FORMAT_ITEMS.forEachIndexed { index, (title, _) -> + formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, title) + } } if (view.selectionStart < view.selectionEnd) { - menu.add(FORMAT_MENU_GROUP_ID, MENU_COPY_MARKDOWN_ID, 101, "Copy as Markdown") + if (selectionMenuConfig.copyAsMarkdown) { + menu.add(FORMAT_MENU_GROUP_ID, MENU_COPY_MARKDOWN_ID, 101, "Copy as Markdown") + } customItemTexts.forEachIndexed { index, text -> menu diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm index ced1c17c..9aa70a76 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm @@ -25,14 +25,15 @@ - (UIMenu *)textView:(UITextView *)textView return nil; } + ENRMInputSelectionMenuConfig menuConfig = [self inputSelectionMenuConfig]; __weak EnrichedMarkdownTextInput *weakSelf = self; + // TODO: Localize titles with NSLocalizedString. static const struct { NSString *title; NSString *icon; ENRMInputStyleType styleType; } kFormatItems[] = { - // TODO: Localize titles with NSLocalizedString. {@"Bold", @"bold", ENRMInputStyleTypeStrong}, {@"Italic", @"italic", ENRMInputStyleTypeEmphasis}, {@"Underline", @"underline", ENRMInputStyleTypeUnderline}, @@ -91,8 +92,13 @@ - (UIMenu *)textView:(UITextView *)textView break; } } - [systemActions insertObject:formatMenu atIndex:insertIndex]; - [systemActions insertObject:copyMarkdownAction atIndex:insertIndex + 1]; + if (menuConfig.format) { + [systemActions insertObject:formatMenu atIndex:insertIndex]; + insertIndex++; + } + if (menuConfig.copyAsMarkdown) { + [systemActions insertObject:copyMarkdownAction atIndex:insertIndex]; + } [allActions addObjectsFromArray:systemActions]; return [UIMenu menuWithChildren:allActions]; @@ -104,6 +110,7 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex return menu; } + ENRMInputSelectionMenuConfig menuConfig = [self inputSelectionMenuConfig]; __weak EnrichedMarkdownTextInput *weakSelf = self; NSArray *customItems = ENRMBuildContextMenuItems([self contextMenuItemTexts], [self contextMenuItemIcons], textView, @@ -114,41 +121,45 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex [menu addItem:[NSMenuItem separatorItem]]; - NSMenuItem *copyMarkdownItem = [[NSMenuItem alloc] initWithTitle:@"Copy as Markdown" - action:@selector(copySelectedRangeAsMarkdown) - keyEquivalent:@""]; - copyMarkdownItem.target = self; - [menu addItem:copyMarkdownItem]; - - NSMenu *formatSubmenu = [[NSMenu alloc] initWithTitle:@"Format"]; - struct { - NSString *title; - SEL action; - NSString *key; - NSEventModifierFlags modifiers; - } const items[] = { - {@"Bold", @selector(toggleBold), @"b", NSEventModifierFlagCommand}, - {@"Italic", @selector(toggleItalic), @"i", NSEventModifierFlagCommand}, - {@"Underline", @selector(toggleUnderline), @"u", NSEventModifierFlagCommand}, - {@"Strikethrough", @selector(toggleStrikethrough), @"", 0}, - {@"Spoiler", @selector(toggleSpoiler), @"", 0}, - {@"Link", @selector(showLinkPrompt), @"", 0}, - }; + if (menuConfig.copyAsMarkdown) { + NSMenuItem *copyMarkdownItem = [[NSMenuItem alloc] initWithTitle:@"Copy as Markdown" + action:@selector(copySelectedRangeAsMarkdown) + keyEquivalent:@""]; + copyMarkdownItem.target = self; + [menu addItem:copyMarkdownItem]; + } - for (NSUInteger i = 0; i < sizeof(items) / sizeof(items[0]); i++) { - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:items[i].title - action:items[i].action - keyEquivalent:items[i].key]; - if (items[i].modifiers) { - item.keyEquivalentModifierMask = items[i].modifiers; + if (menuConfig.format) { + NSMenu *formatSubmenu = [[NSMenu alloc] initWithTitle:@"Format"]; + struct { + NSString *title; + SEL action; + NSString *key; + NSEventModifierFlags modifiers; + } const items[] = { + {@"Bold", @selector(toggleBold), @"b", NSEventModifierFlagCommand}, + {@"Italic", @selector(toggleItalic), @"i", NSEventModifierFlagCommand}, + {@"Underline", @selector(toggleUnderline), @"u", NSEventModifierFlagCommand}, + {@"Strikethrough", @selector(toggleStrikethrough), @"", 0}, + {@"Spoiler", @selector(toggleSpoiler), @"", 0}, + {@"Link", @selector(showLinkPrompt), @"", 0}, + }; + + for (NSUInteger i = 0; i < sizeof(items) / sizeof(items[0]); i++) { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:items[i].title + action:items[i].action + keyEquivalent:items[i].key]; + if (items[i].modifiers) { + item.keyEquivalentModifierMask = items[i].modifiers; + } + item.target = self; + [formatSubmenu addItem:item]; } - item.target = self; - [formatSubmenu addItem:item]; - } - NSMenuItem *formatItem = [[NSMenuItem alloc] initWithTitle:@"Format" action:nil keyEquivalent:@""]; - formatItem.submenu = formatSubmenu; - [menu addItem:formatItem]; + NSMenuItem *formatItem = [[NSMenuItem alloc] initWithTitle:@"Format" action:nil keyEquivalent:@""]; + formatItem.submenu = formatSubmenu; + [menu addItem:formatItem]; + } return menu; } diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h index f02ae612..4319880a 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h @@ -5,6 +5,11 @@ NS_ASSUME_NONNULL_BEGIN +typedef struct { + BOOL format; + BOOL copyAsMarkdown; +} ENRMInputSelectionMenuConfig; + @interface EnrichedMarkdownTextInput (Internal) - (void)toggleBold; @@ -20,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)emitContextMenuItemPress:(NSString *)itemText; - (NSArray *)contextMenuItemTexts; - (NSArray *)contextMenuItemIcons; +- (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig; #if TARGET_OS_OSX - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu textView:(NSTextView *)textView; diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm index bbf822b5..c243edaa 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm @@ -1,5 +1,4 @@ #import "EnrichedMarkdownTextInput.h" -#import #import "ContextMenuUtils.h" #import "ENRMAutoLinkDetector.h" #import "ENRMDetectorPipeline.h" @@ -17,9 +16,11 @@ #import "ENRMStyleMergingConfig.h" #import "ENRMUIKit.h" #import "ENRMWordsUtils.h" +#import "EnrichedMarkdownTextInput+Internal.h" #import "InputStylePropsUtils.h" #import "ParagraphStyleUtils.h" #import "SelectionColorUtils.h" +#import #import #if TARGET_OS_OSX #import @@ -91,6 +92,8 @@ @implementation EnrichedMarkdownTextInput { ENRMWritingDirectionMode _writingDirectionMode; NSWritingDirection _resolvedLayoutDirection; + + ENRMInputSelectionMenuConfig _inputSelectionMenuConfig; } #pragma mark - Fabric lifecycle @@ -128,6 +131,7 @@ - (instancetype)initWithFrame:(CGRect)frame _writingDirectionMode = ENRMWritingDirectionModeFirstStrong; _resolvedLayoutDirection = [[RCTI18nUtil sharedInstance] isRTL] ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight; + _inputSelectionMenuConfig = (ENRMInputSelectionMenuConfig){.format = YES, .copyAsMarkdown = YES}; [self setupTextView]; @@ -352,6 +356,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems); } + _inputSelectionMenuConfig = (ENRMInputSelectionMenuConfig){ + .format = newViewProps.selectionMenuConfig.format, + .copyAsMarkdown = newViewProps.selectionMenuConfig.copyAsMarkdown, + }; + if (newViewProps.mentionIndicators != oldViewProps.mentionIndicators) { NSMutableArray *indicators = [NSMutableArray array]; for (const auto &indicator : newViewProps.mentionIndicators) { @@ -964,8 +973,8 @@ - (void)resetPendingStylesForSelectionChange // Skip system-driven selection adjustments (e.g., predictive text) that fire // immediately after a text edit. static const CFTimeInterval kPostEditGracePeriod = 0.1; - BOOL isPostEditAdjustment = (_lastTextChangeTime > 0 && - (CACurrentMediaTime() - _lastTextChangeTime) < kPostEditGracePeriod); + BOOL isPostEditAdjustment = + (_lastTextChangeTime > 0 && (CACurrentMediaTime() - _lastTextChangeTime) < kPostEditGracePeriod); if (isPostEditAdjustment) { return; } @@ -983,11 +992,8 @@ - (void)rebuildPendingStylesFromContext } static const ENRMInputStyleType inlineStyles[] = { - ENRMInputStyleTypeStrong, - ENRMInputStyleTypeEmphasis, - ENRMInputStyleTypeUnderline, - ENRMInputStyleTypeStrikethrough, - ENRMInputStyleTypeSpoiler, + ENRMInputStyleTypeStrong, ENRMInputStyleTypeEmphasis, ENRMInputStyleTypeUnderline, + ENRMInputStyleTypeStrikethrough, ENRMInputStyleTypeSpoiler, }; for (NSUInteger i = 0; i < sizeof(inlineStyles) / sizeof(inlineStyles[0]); i++) { @@ -1236,6 +1242,11 @@ - (void)emitCaretRectChangeIfNeeded return _contextMenuItemIcons ?: @[]; } +- (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig +{ + return _inputSelectionMenuConfig; +} + - (void)emitContextMenuItemPress:(NSString *)itemText { auto eventEmitter = [self getEventEmitter]; diff --git a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx index 017ea3b4..3aaa5f6d 100644 --- a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx +++ b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx @@ -112,6 +112,21 @@ export interface EnrichedMarkdownTextInputInstance { getCaretRect: () => Promise; } +export interface InputSelectionMenuConfig { + /** + * Shows the built-in "Format" submenu (Bold, Italic, Underline, etc.) + * in the text selection context menu. + * @default true + */ + format?: boolean; + /** + * Shows the built-in "Copy as Markdown" action in the text selection + * context menu. + * @default true + */ + copyAsMarkdown?: boolean; +} + export interface EnrichedMarkdownTextInputProps extends Omit< ViewProps, 'style' | 'children' @@ -142,6 +157,14 @@ export interface EnrichedMarkdownTextInputProps extends Omit< onFocus?: () => void; onBlur?: () => void; contextMenuItems?: ContextMenuItem[]; + /** + * Controls built-in items in the text selection context menu. + * Omitting the prop or any field reproduces today's exact menu. + * Custom app-provided actions are controlled separately via `contextMenuItems`. + * @default { format: true, copyAsMarkdown: true } + * @platform ios, android, macos + */ + selectionMenuConfig?: InputSelectionMenuConfig; linkRegex?: RegExp | null; /** * Paragraph writing direction. @@ -203,6 +226,7 @@ export const EnrichedMarkdownTextInput = ({ onFocus, onBlur, contextMenuItems, + selectionMenuConfig, linkRegex: _linkRegex, writingDirection = 'first-strong', ...rest @@ -251,6 +275,14 @@ export const EnrichedMarkdownTextInput = ({ const normalizedStyle = normalizeMarkdownTextInputStyle(markdownStyle); + const normalizedSelectionMenuConfig = useMemo( + () => ({ + format: selectionMenuConfig?.format ?? true, + copyAsMarkdown: selectionMenuConfig?.copyAsMarkdown ?? true, + }), + [selectionMenuConfig] + ); + const linkRegex = useMemo( () => toNativeRegexConfig(_linkRegex), [_linkRegex] @@ -461,6 +493,7 @@ export const EnrichedMarkdownTextInput = ({ handleCaretRectChange as NativeProps['onCaretRectChange'] } contextMenuItems={nativeContextMenuItems} + selectionMenuConfig={normalizedSelectionMenuConfig} mentionIndicators={mentionIndicators} onContextMenuItemPress={ handleContextMenuItemPress as NativeProps['onContextMenuItemPress'] diff --git a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts index 041435dd..46292345 100644 --- a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts +++ b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts @@ -113,6 +113,11 @@ export interface ContextMenuItemConfig { icon?: string; } +export interface InputSelectionMenuConfigInternal { + format: boolean; + copyAsMarkdown: boolean; +} + export interface OnContextMenuItemPressEvent { itemText: string; selectedText: string; @@ -198,6 +203,12 @@ export interface NativeProps extends ViewProps { */ contextMenuItems?: ReadonlyArray>; + /** + * Controls built-in items in the text selection context menu. + * `format` toggles the Format submenu; `copyAsMarkdown` toggles the Copy as Markdown action. + */ + selectionMenuConfig: Readonly; + /** * Regex configuration for automatic link detection. * Omit or pass undefined to use platform defaults. diff --git a/packages/react-native-enriched-markdown/src/index.tsx b/packages/react-native-enriched-markdown/src/index.tsx index b180ff42..4d7ac2b9 100644 --- a/packages/react-native-enriched-markdown/src/index.tsx +++ b/packages/react-native-enriched-markdown/src/index.tsx @@ -20,6 +20,7 @@ export type { MarkdownTextInputStyle, StyleState, ContextMenuItem, + InputSelectionMenuConfig, OnLinkDetected, OnStartMentionEvent, OnChangeMentionEvent, From d7d92e87e346874bd9b64af573aaa70d11d44f8a Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Thu, 18 Jun 2026 21:41:38 +0200 Subject: [PATCH 2/5] feat: add formatMenuConfig prop for per-item format submenu control (#407) --- apps/example/ios/Podfile.lock | 6 +-- docs/API_REFERENCE.md | 30 +++++++++++++++ .../input/EnrichedMarkdownTextInputManager.kt | 18 +++++++++ .../input/toolbar/InputContextMenu.kt | 26 ++++++++++++- .../EnrichedMarkdownTextInput+ContextMenu.mm | 12 ++++++ .../EnrichedMarkdownTextInput+Internal.h | 10 +++++ .../ios/input/EnrichedMarkdownTextInput.mm | 17 +++++++++ .../src/EnrichedMarkdownTextInput.tsx | 37 +++++++++++++++++++ ...nrichedMarkdownTextInputNativeComponent.ts | 14 +++++++ .../src/index.tsx | 1 + 10 files changed, 166 insertions(+), 5 deletions(-) diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 878c830b..5643045a 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2489,7 +2489,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d - hermes-engine: 7a5738e17537a638701b58a7488ab83a72cddf11 + hermes-engine: 52eaa7366787880ce4ab803bec15b9b969e72d09 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 @@ -2498,7 +2498,7 @@ SPEC CHECKSUMS: React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48 React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0 - React-Core-prebuilt: 300f1f7da5b63a2c480bde8dfc5cde8843905766 + React-Core-prebuilt: 76677a1d1314f90310d9d1968b2d71c229861228 React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146 React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716 React-debug: 9af1e96a6069c996e3d9f1e493603e74bc9f1593 @@ -2562,7 +2562,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 ReactCodegen: 7016a2114079361a2f1536b3c91a15ceb8eb7ca4 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f - ReactNativeDependencies: c6c22887dcb9997b770c6751e2a8b7c528e29363 + ReactNativeDependencies: b8ec997507405a87f23927c21c3693d5859efb9a ReactNativeEnrichedMarkdown: 15ee1297ff2df94e80d06b7d86c13e5fbbcb806a RNCAsyncStorage: 7d913885caaae13f5fb62dd0bd04c674ea8bf97b RNDateTimePicker: afe0288e6c7855994f7f2617b7b2c05799fea7f3 diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index bebb312b..1ca0660f 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -722,6 +722,36 @@ interface InputSelectionMenuConfig { /> ``` +### `formatMenuConfig` + +Controls which individual items appear inside the Format submenu. Only effective when `selectionMenuConfig.format` is `true` (the default). Omitting the prop or any field shows all items. + +| Type | Default Value | Platform | +| ------------------ | ------------------------------------------------------------------------------------ | ------------------- | +| `FormatMenuConfig` | `{ bold: true, italic: true, underline: true, strikethrough: true, spoiler: true, link: true }` | iOS, Android, macOS | + +**`FormatMenuConfig` shape:** + +```ts +interface FormatMenuConfig { + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + spoiler?: boolean; + link?: boolean; +} +``` + +**Example:** + +```tsx +// Hide Spoiler and Link from the Format submenu + +``` + ### Ref Methods All methods are called imperatively on the ref (`ref.current?.methodName()`). diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt index 0352d177..2c7eb535 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt @@ -31,6 +31,7 @@ import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent import com.swmansion.enriched.markdown.input.layout.InputMeasurementStore import com.swmansion.enriched.markdown.input.model.StyleType +import com.swmansion.enriched.markdown.input.toolbar.FormatMenuConfig import com.swmansion.enriched.markdown.input.toolbar.InputSelectionMenuConfig import com.swmansion.enriched.markdown.utils.input.BorderPropsApplicator import com.swmansion.enriched.markdown.utils.input.MarkdownStyleParser @@ -277,6 +278,23 @@ class EnrichedMarkdownTextInputManager : ) } + @ReactProp(name = "formatMenuConfig") + override fun setFormatMenuConfig( + view: EnrichedMarkdownTextInputView?, + value: ReadableMap?, + ) { + if (view == null || value == null) return + view.contextMenu.formatMenuConfig = + FormatMenuConfig( + bold = value.getBoolean("bold"), + italic = value.getBoolean("italic"), + underline = value.getBoolean("underline"), + strikethrough = value.getBoolean("strikethrough"), + spoiler = value.getBoolean("spoiler"), + link = value.getBoolean("link"), + ) + } + @ReactProp(name = "linkRegex") override fun setLinkRegex( view: EnrichedMarkdownTextInputView?, diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt index f2ec225d..4dc215fa 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt @@ -21,11 +21,21 @@ data class InputSelectionMenuConfig( val copyAsMarkdown: Boolean = true, ) +data class FormatMenuConfig( + val bold: Boolean = true, + val italic: Boolean = true, + val underline: Boolean = true, + val strikethrough: Boolean = true, + val spoiler: Boolean = true, + val link: Boolean = true, +) + class InputContextMenu( private val view: EnrichedMarkdownTextInputView, ) { private var customItemTexts: List = emptyList() var selectionMenuConfig: InputSelectionMenuConfig = InputSelectionMenuConfig() + var formatMenuConfig: FormatMenuConfig = FormatMenuConfig() fun setContextMenuItems(items: List) { customItemTexts = items @@ -50,8 +60,10 @@ class InputContextMenu( if (selectionMenuConfig.format) { val formatSubMenu = menu.addSubMenu(FORMAT_MENU_GROUP_ID, MENU_FORMAT_ID, 100, "Format") - FORMAT_ITEMS.forEachIndexed { index, (title, _) -> - formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, title) + FORMAT_ITEMS.forEachIndexed { index, (title, styleType) -> + if (isFormatItemVisible(styleType)) { + formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, title) + } } } @@ -106,6 +118,16 @@ class InputContextMenu( } } + private fun isFormatItemVisible(styleType: StyleType): Boolean = + when (styleType) { + StyleType.BOLD -> formatMenuConfig.bold + StyleType.ITALIC -> formatMenuConfig.italic + StyleType.UNDERLINE -> formatMenuConfig.underline + StyleType.STRIKETHROUGH -> formatMenuConfig.strikethrough + StyleType.SPOILER -> formatMenuConfig.spoiler + StyleType.LINK -> formatMenuConfig.link + } + private fun applyFormat(styleType: StyleType) { val start = view.selectionStart val end = view.selectionEnd diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm index 9aa70a76..bde4d664 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm @@ -26,6 +26,7 @@ - (UIMenu *)textView:(UITextView *)textView } ENRMInputSelectionMenuConfig menuConfig = [self inputSelectionMenuConfig]; + ENRMFormatMenuConfig fmtConfig = [self formatMenuConfig]; __weak EnrichedMarkdownTextInput *weakSelf = self; // TODO: Localize titles with NSLocalizedString. @@ -42,9 +43,14 @@ - (UIMenu *)textView:(UITextView *)textView {@"Link", @"link", ENRMInputStyleTypeLink}, }; static const NSUInteger kFormatItemCount = sizeof(kFormatItems) / sizeof(kFormatItems[0]); + const BOOL kFormatItemVisible[] = {fmtConfig.bold, fmtConfig.italic, fmtConfig.underline, + fmtConfig.strikethrough, fmtConfig.spoiler, fmtConfig.link}; NSMutableArray *formatActions = [NSMutableArray arrayWithCapacity:kFormatItemCount]; for (NSUInteger i = 0; i < kFormatItemCount; i++) { + if (!kFormatItemVisible[i]) { + continue; + } ENRMInputStyleType styleType = kFormatItems[i].styleType; UIAction *action = [UIAction actionWithTitle:kFormatItems[i].title image:[UIImage systemImageNamed:kFormatItems[i].icon] @@ -111,6 +117,7 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex } ENRMInputSelectionMenuConfig menuConfig = [self inputSelectionMenuConfig]; + ENRMFormatMenuConfig fmtConfig = [self formatMenuConfig]; __weak EnrichedMarkdownTextInput *weakSelf = self; NSArray *customItems = ENRMBuildContextMenuItems([self contextMenuItemTexts], [self contextMenuItemIcons], textView, @@ -144,8 +151,13 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex {@"Spoiler", @selector(toggleSpoiler), @"", 0}, {@"Link", @selector(showLinkPrompt), @"", 0}, }; + const BOOL visible[] = {fmtConfig.bold, fmtConfig.italic, fmtConfig.underline, + fmtConfig.strikethrough, fmtConfig.spoiler, fmtConfig.link}; for (NSUInteger i = 0; i < sizeof(items) / sizeof(items[0]); i++) { + if (!visible[i]) { + continue; + } NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:items[i].title action:items[i].action keyEquivalent:items[i].key]; diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h index 4319880a..25fcd1fc 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+Internal.h @@ -10,6 +10,15 @@ typedef struct { BOOL copyAsMarkdown; } ENRMInputSelectionMenuConfig; +typedef struct { + BOOL bold; + BOOL italic; + BOOL underline; + BOOL strikethrough; + BOOL spoiler; + BOOL link; +} ENRMFormatMenuConfig; + @interface EnrichedMarkdownTextInput (Internal) - (void)toggleBold; @@ -26,6 +35,7 @@ typedef struct { - (NSArray *)contextMenuItemTexts; - (NSArray *)contextMenuItemIcons; - (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig; +- (ENRMFormatMenuConfig)formatMenuConfig; #if TARGET_OS_OSX - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu textView:(NSTextView *)textView; diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm index c243edaa..442cfa45 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput.mm @@ -94,6 +94,7 @@ @implementation EnrichedMarkdownTextInput { NSWritingDirection _resolvedLayoutDirection; ENRMInputSelectionMenuConfig _inputSelectionMenuConfig; + ENRMFormatMenuConfig _formatMenuConfig; } #pragma mark - Fabric lifecycle @@ -132,6 +133,8 @@ - (instancetype)initWithFrame:(CGRect)frame _resolvedLayoutDirection = [[RCTI18nUtil sharedInstance] isRTL] ? NSWritingDirectionRightToLeft : NSWritingDirectionLeftToRight; _inputSelectionMenuConfig = (ENRMInputSelectionMenuConfig){.format = YES, .copyAsMarkdown = YES}; + _formatMenuConfig = (ENRMFormatMenuConfig){ + .bold = YES, .italic = YES, .underline = YES, .strikethrough = YES, .spoiler = YES, .link = YES}; [self setupTextView]; @@ -361,6 +364,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & .copyAsMarkdown = newViewProps.selectionMenuConfig.copyAsMarkdown, }; + _formatMenuConfig = (ENRMFormatMenuConfig){ + .bold = newViewProps.formatMenuConfig.bold, + .italic = newViewProps.formatMenuConfig.italic, + .underline = newViewProps.formatMenuConfig.underline, + .strikethrough = newViewProps.formatMenuConfig.strikethrough, + .spoiler = newViewProps.formatMenuConfig.spoiler, + .link = newViewProps.formatMenuConfig.link, + }; + if (newViewProps.mentionIndicators != oldViewProps.mentionIndicators) { NSMutableArray *indicators = [NSMutableArray array]; for (const auto &indicator : newViewProps.mentionIndicators) { @@ -1247,6 +1259,11 @@ - (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig return _inputSelectionMenuConfig; } +- (ENRMFormatMenuConfig)formatMenuConfig +{ + return _formatMenuConfig; +} + - (void)emitContextMenuItemPress:(NSString *)itemText { auto eventEmitter = [self getEventEmitter]; diff --git a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx index 3aaa5f6d..33126514 100644 --- a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx +++ b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInput.tsx @@ -127,6 +127,21 @@ export interface InputSelectionMenuConfig { copyAsMarkdown?: boolean; } +export interface FormatMenuConfig { + /** @default true */ + bold?: boolean; + /** @default true */ + italic?: boolean; + /** @default true */ + underline?: boolean; + /** @default true */ + strikethrough?: boolean; + /** @default true */ + spoiler?: boolean; + /** @default true */ + link?: boolean; +} + export interface EnrichedMarkdownTextInputProps extends Omit< ViewProps, 'style' | 'children' @@ -165,6 +180,14 @@ export interface EnrichedMarkdownTextInputProps extends Omit< * @platform ios, android, macos */ selectionMenuConfig?: InputSelectionMenuConfig; + /** + * Controls which items appear inside the Format submenu. + * Only effective when `selectionMenuConfig.format` is `true` (the default). + * Omitting the prop or any field shows all items. + * @default { bold: true, italic: true, underline: true, strikethrough: true, spoiler: true, link: true } + * @platform ios, android, macos + */ + formatMenuConfig?: FormatMenuConfig; linkRegex?: RegExp | null; /** * Paragraph writing direction. @@ -227,6 +250,7 @@ export const EnrichedMarkdownTextInput = ({ onBlur, contextMenuItems, selectionMenuConfig, + formatMenuConfig, linkRegex: _linkRegex, writingDirection = 'first-strong', ...rest @@ -283,6 +307,18 @@ export const EnrichedMarkdownTextInput = ({ [selectionMenuConfig] ); + const normalizedFormatMenuConfig = useMemo( + () => ({ + bold: formatMenuConfig?.bold ?? true, + italic: formatMenuConfig?.italic ?? true, + underline: formatMenuConfig?.underline ?? true, + strikethrough: formatMenuConfig?.strikethrough ?? true, + spoiler: formatMenuConfig?.spoiler ?? true, + link: formatMenuConfig?.link ?? true, + }), + [formatMenuConfig] + ); + const linkRegex = useMemo( () => toNativeRegexConfig(_linkRegex), [_linkRegex] @@ -494,6 +530,7 @@ export const EnrichedMarkdownTextInput = ({ } contextMenuItems={nativeContextMenuItems} selectionMenuConfig={normalizedSelectionMenuConfig} + formatMenuConfig={normalizedFormatMenuConfig} mentionIndicators={mentionIndicators} onContextMenuItemPress={ handleContextMenuItemPress as NativeProps['onContextMenuItemPress'] diff --git a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts index 46292345..282e5238 100644 --- a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts +++ b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextInputNativeComponent.ts @@ -118,6 +118,15 @@ export interface InputSelectionMenuConfigInternal { copyAsMarkdown: boolean; } +export interface FormatMenuConfigInternal { + bold: boolean; + italic: boolean; + underline: boolean; + strikethrough: boolean; + spoiler: boolean; + link: boolean; +} + export interface OnContextMenuItemPressEvent { itemText: string; selectedText: string; @@ -209,6 +218,11 @@ export interface NativeProps extends ViewProps { */ selectionMenuConfig: Readonly; + /** + * Controls which items appear inside the Format submenu. + */ + formatMenuConfig: Readonly; + /** * Regex configuration for automatic link detection. * Omit or pass undefined to use platform defaults. diff --git a/packages/react-native-enriched-markdown/src/index.tsx b/packages/react-native-enriched-markdown/src/index.tsx index 4d7ac2b9..e4d315ec 100644 --- a/packages/react-native-enriched-markdown/src/index.tsx +++ b/packages/react-native-enriched-markdown/src/index.tsx @@ -21,6 +21,7 @@ export type { StyleState, ContextMenuItem, InputSelectionMenuConfig, + FormatMenuConfig, OnLinkDetected, OnStartMentionEvent, OnChangeMentionEvent, From e637e9463737fd74c457f0187a877e46b50ac093 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 19 Jun 2026 11:25:05 +0200 Subject: [PATCH 3/5] fix: handle null value in selectionMenuConfig for EnrichedMarkdownTextInput --- .../input/EnrichedMarkdownTextInputManager.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt index 0352d177..391d5061 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt @@ -269,12 +269,16 @@ class EnrichedMarkdownTextInputManager : view: EnrichedMarkdownTextInputView?, value: ReadableMap?, ) { - if (view == null || value == null) return + if (view == null) return view.contextMenu.selectionMenuConfig = - InputSelectionMenuConfig( - format = value.getBoolean("format"), - copyAsMarkdown = value.getBoolean("copyAsMarkdown"), - ) + if (value == null) { + InputSelectionMenuConfig() + } else { + InputSelectionMenuConfig( + format = value.getBoolean("format"), + copyAsMarkdown = value.getBoolean("copyAsMarkdown"), + ) + } } @ReactProp(name = "linkRegex") From 5b975f9286b5b9f2caa0d7026a87acc7415a94a9 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 19 Jun 2026 11:54:49 +0200 Subject: [PATCH 4/5] feat: add story for SelectionMenu in EnrichedMarkdownTextInput --- .../props/SelectionMenu.stories.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/SelectionMenu.stories.tsx diff --git a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/SelectionMenu.stories.tsx b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/SelectionMenu.stories.tsx new file mode 100644 index 00000000..9585b415 --- /dev/null +++ b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/SelectionMenu.stories.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { EnrichedMarkdownTextInputStory } from '../EnrichedMarkdownTextInputStory'; +import { storyMeta } from '../shared/storyMeta'; +import type { InputStory } from '../shared/storyTypes'; + +type SelectionMenuStoryExtra = { + format: boolean; + copyAsMarkdown: boolean; +}; + +const MARKDOWN = + 'Select this text and open the context menu to see the built-in actions.'; + +const argTypes = { + format: { + control: 'boolean', + description: + 'selectionMenuConfig.format — show the "Format" submenu (Bold, Italic, etc.) in the selection menu.', + }, + copyAsMarkdown: { + control: 'boolean', + description: + 'selectionMenuConfig.copyAsMarkdown — show "Copy as Markdown" in the selection menu.', + }, +}; + +export default storyMeta('Props', 'Selection Menu'); + +export const Default: InputStory = { + args: { + initialMarkdown: MARKDOWN, + format: true, + copyAsMarkdown: true, + }, + argTypes, + render: ({ format, copyAsMarkdown, ...args }) => ( + + ), +}; From 9ca3724c9aa51974736053ba3fc98751a9488a7d Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 19 Jun 2026 12:03:22 +0200 Subject: [PATCH 5/5] feat: add FormatMenu story and improve null handling in formatMenuConfig --- .../props/FormatMenu.stories.tsx | 85 +++++++++++++++++++ .../input/EnrichedMarkdownTextInputManager.kt | 22 +++-- .../EnrichedMarkdownTextInput+ContextMenu.mm | 5 ++ 3 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/FormatMenu.stories.tsx diff --git a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/FormatMenu.stories.tsx b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/FormatMenu.stories.tsx new file mode 100644 index 00000000..3b2f3e67 --- /dev/null +++ b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownTextInput/props/FormatMenu.stories.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { EnrichedMarkdownTextInputStory } from '../EnrichedMarkdownTextInputStory'; +import { storyMeta } from '../shared/storyMeta'; +import type { InputStory } from '../shared/storyTypes'; + +type FormatMenuStoryExtra = { + bold: boolean; + italic: boolean; + underline: boolean; + strikethrough: boolean; + spoiler: boolean; + link: boolean; +}; + +const MARKDOWN = + 'Select this text and open the Format submenu to see which items are visible.'; + +const argTypes = { + bold: { + control: 'boolean', + description: 'formatMenuConfig.bold — show "Bold" in the Format submenu.', + }, + italic: { + control: 'boolean', + description: + 'formatMenuConfig.italic — show "Italic" in the Format submenu.', + }, + underline: { + control: 'boolean', + description: + 'formatMenuConfig.underline — show "Underline" in the Format submenu.', + }, + strikethrough: { + control: 'boolean', + description: + 'formatMenuConfig.strikethrough — show "Strikethrough" in the Format submenu.', + }, + spoiler: { + control: 'boolean', + description: + 'formatMenuConfig.spoiler — show "Spoiler" in the Format submenu.', + }, + link: { + control: 'boolean', + description: 'formatMenuConfig.link — show "Link" in the Format submenu.', + }, +}; + +export default storyMeta('Props', 'Format Menu'); + +export const Default: InputStory = { + args: { + initialMarkdown: MARKDOWN, + bold: true, + italic: true, + underline: true, + strikethrough: true, + spoiler: true, + link: true, + }, + argTypes, + render: ({ + bold, + italic, + underline, + strikethrough, + spoiler, + link, + ...args + }) => ( + + ), +}; diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt index 67284771..a52be906 100644 --- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt @@ -287,16 +287,20 @@ class EnrichedMarkdownTextInputManager : view: EnrichedMarkdownTextInputView?, value: ReadableMap?, ) { - if (view == null || value == null) return + if (view == null) return view.contextMenu.formatMenuConfig = - FormatMenuConfig( - bold = value.getBoolean("bold"), - italic = value.getBoolean("italic"), - underline = value.getBoolean("underline"), - strikethrough = value.getBoolean("strikethrough"), - spoiler = value.getBoolean("spoiler"), - link = value.getBoolean("link"), - ) + if (value == null) { + FormatMenuConfig() + } else { + FormatMenuConfig( + bold = value.getBoolean("bold"), + italic = value.getBoolean("italic"), + underline = value.getBoolean("underline"), + strikethrough = value.getBoolean("strikethrough"), + spoiler = value.getBoolean("spoiler"), + link = value.getBoolean("link"), + ) + } } @ReactProp(name = "linkRegex") diff --git a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm index bde4d664..1beb4265 100644 --- a/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +++ b/packages/react-native-enriched-markdown/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm @@ -45,6 +45,9 @@ - (UIMenu *)textView:(UITextView *)textView static const NSUInteger kFormatItemCount = sizeof(kFormatItems) / sizeof(kFormatItems[0]); const BOOL kFormatItemVisible[] = {fmtConfig.bold, fmtConfig.italic, fmtConfig.underline, fmtConfig.strikethrough, fmtConfig.spoiler, fmtConfig.link}; + _Static_assert(sizeof(kFormatItemVisible) / sizeof(kFormatItemVisible[0]) == + sizeof(kFormatItems) / sizeof(kFormatItems[0]), + "kFormatItemVisible must match kFormatItems length"); NSMutableArray *formatActions = [NSMutableArray arrayWithCapacity:kFormatItemCount]; for (NSUInteger i = 0; i < kFormatItemCount; i++) { @@ -153,6 +156,8 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex }; const BOOL visible[] = {fmtConfig.bold, fmtConfig.italic, fmtConfig.underline, fmtConfig.strikethrough, fmtConfig.spoiler, fmtConfig.link}; + _Static_assert(sizeof(visible) / sizeof(visible[0]) == sizeof(items) / sizeof(items[0]), + "visible must match items length"); for (NSUInteger i = 0; i < sizeof(items) / sizeof(items[0]); i++) { if (!visible[i]) {