diff --git a/super_editor/.run/Flutter - Text Field.run.xml b/super_editor/.run/Flutter - Text Field.run.xml
new file mode 100644
index 000000000..bc96023a3
--- /dev/null
+++ b/super_editor/.run/Flutter - Text Field.run.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart b/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart
new file mode 100644
index 000000000..d91b07a5b
--- /dev/null
+++ b/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart
@@ -0,0 +1,183 @@
+import 'package:example/demos/in_the_lab/in_the_lab_scaffold.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:follow_the_leader/follow_the_leader.dart';
+import 'package:super_editor/super_editor.dart';
+
+/// Super Editor demo that uses the native iOS context menu as the floating toolbar
+/// for both Super Editor and Super Text Field.
+///
+/// By default, Super Editor and Super Text Field display a floating toolbar that's
+/// painted by Flutter. By using Flutter, you gain full control over appearance, and
+/// the available options. However, recent versions of iOS have security settings
+/// that bring up an annoying warning if you attempt to run a "paste" command without
+/// using their native iOS toolbar. For that reason, Super Editor makes it possible
+/// to show the native iOS toolbar.
+class NativeIosContextMenuFeatureDemo extends StatefulWidget {
+ const NativeIosContextMenuFeatureDemo({super.key});
+
+ @override
+ State createState() => _NativeIosContextMenuFeatureDemoState();
+}
+
+class _NativeIosContextMenuFeatureDemoState extends State {
+ final _documentLayoutKey = GlobalKey();
+
+ late final MutableDocument _document;
+ late final MutableDocumentComposer _composer;
+ late final Editor _editor;
+ late final CommonEditorOperations _commonEditorOperations;
+
+ late final SuperEditorIosControlsController _toolbarController;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _document = MutableDocument.empty();
+ _composer = MutableDocumentComposer();
+ _editor = Editor(
+ editables: {
+ Editor.documentKey: _document,
+ Editor.composerKey: _composer,
+ },
+ requestHandlers: [
+ ...defaultRequestHandlers,
+ ],
+ );
+ _commonEditorOperations = CommonEditorOperations(
+ document: _document,
+ editor: _editor,
+ composer: _composer,
+ documentLayoutResolver: () => _documentLayoutKey.currentState as DocumentLayout,
+ );
+
+ _toolbarController = SuperEditorIosControlsController(
+ toolbarBuilder: _buildToolbar,
+ );
+ }
+
+ @override
+ void dispose() {
+ _toolbarController.dispose();
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return InTheLabScaffold(
+ content: _buildEditor(),
+ supplemental: _buildTextField(),
+ );
+ }
+
+ Widget _buildEditor() {
+ return SuperEditorIosControlsScope(
+ controller: _toolbarController,
+ child: IntrinsicHeight(
+ child: SuperEditor(
+ editor: _editor,
+ documentLayoutKey: _documentLayoutKey,
+ selectionStyle: SelectionStyles(
+ selectionColor: Colors.red.withOpacity(0.3),
+ ),
+ stylesheet: defaultStylesheet.copyWith(
+ addRulesAfter: [
+ ...darkModeStyles,
+ ],
+ ),
+ documentOverlayBuilders: [
+ if (defaultTargetPlatform == TargetPlatform.iOS) ...[
+ // Adds a Leader around the document selection at a focal point for the
+ // iOS floating toolbar.
+ SuperEditorIosToolbarFocalPointDocumentLayerBuilder(),
+ // Displays caret and drag handles, specifically for iOS.
+ SuperEditorIosHandlesDocumentLayerBuilder(
+ handleColor: Colors.red,
+ ),
+ ],
+
+ if (defaultTargetPlatform == TargetPlatform.android) ...[
+ // Adds a Leader around the document selection at a focal point for the
+ // Android floating toolbar.
+ SuperEditorAndroidToolbarFocalPointDocumentLayerBuilder(),
+ // Displays caret and drag handles, specifically for Android.
+ SuperEditorAndroidHandlesDocumentLayerBuilder(
+ caretColor: Colors.red,
+ ),
+ ],
+
+ // Displays caret for typical desktop use-cases.
+ DefaultCaretOverlayBuilder(
+ caretStyle: const CaretStyle().copyWith(color: Colors.redAccent),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildToolbar(
+ BuildContext context,
+ Key mobileToolbarKey,
+ LeaderLink focalPoint,
+ ) {
+ return iOSSystemPopoverEditorToolbarWithFallbackBuilder(
+ context,
+ mobileToolbarKey,
+ focalPoint,
+ _commonEditorOperations,
+ SuperEditorIosControlsScope.rootOf(context),
+ );
+ }
+
+ Widget _buildTextField() {
+ return Padding(
+ padding: const EdgeInsets.all(24),
+ child: _SuperTextFieldWithNativeContextMenu(),
+ );
+ }
+}
+
+class _SuperTextFieldWithNativeContextMenu extends StatefulWidget {
+ const _SuperTextFieldWithNativeContextMenu({Key? key}) : super(key: key);
+
+ @override
+ State<_SuperTextFieldWithNativeContextMenu> createState() => _SuperTextFieldWithNativeContextMenuState();
+}
+
+class _SuperTextFieldWithNativeContextMenuState extends State<_SuperTextFieldWithNativeContextMenu> {
+ @override
+ Widget build(BuildContext context) {
+ return DecoratedBox(
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: SuperIOSTextField(
+ padding: const EdgeInsets.all(12),
+ caretStyle: CaretStyle(color: Colors.red),
+ selectionColor: defaultSelectionColor,
+ handlesColor: Colors.red,
+ textStyleBuilder: (attributions) {
+ return defaultTextFieldStyleBuilder(attributions).copyWith(
+ color: Colors.white,
+ fontSize: 18,
+ );
+ },
+ hintBehavior: HintBehavior.displayHintUntilTextEntered,
+ hintBuilder: (_) {
+ return Text(
+ "Enter text and open toolbar",
+ style: TextStyle(
+ color: Colors.grey,
+ fontSize: 18,
+ ),
+ );
+ },
+ popoverToolbarBuilder: iOSSystemPopoverTextFieldToolbarWithFallback,
+ ),
+ );
+ }
+}
diff --git a/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart b/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart
index 120121381..fc39e28be 100644
--- a/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart
+++ b/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart
@@ -68,27 +68,27 @@ class _HashTagsFeatureDemoState extends State {
inlineTextStyler: (attributions, existingStyle) {
TextStyle style = defaultInlineTextStyler(attributions, existingStyle);
- if (attributions.whereType().isNotEmpty) {
- style = style.copyWith(
- color: Colors.orange,
- );
- }
-
- return style;
- },
- addRulesAfter: [
- ...darkModeStyles,
- ],
- ),
- documentOverlayBuilders: [
- DefaultCaretOverlayBuilder(
- caretStyle: CaretStyle().copyWith(color: Colors.redAccent),
+ if (attributions.whereType().isNotEmpty) {
+ style = style.copyWith(
+ color: Colors.orange,
+ );
+ }
+
+ return style;
+ },
+ addRulesAfter: [
+ ...darkModeStyles,
+ ],
),
- ],
- plugins: {
- _hashTagPlugin,
- },
- );
+ documentOverlayBuilders: [
+ DefaultCaretOverlayBuilder(
+ caretStyle: CaretStyle().copyWith(color: Colors.redAccent),
+ ),
+ ],
+ plugins: {
+ _hashTagPlugin,
+ },
+ );
}
Widget _buildTagList() {
diff --git a/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart b/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart
index 2a63470ce..b78395df5 100644
--- a/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart
+++ b/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart
@@ -30,15 +30,7 @@ class InTheLabScaffold extends StatelessWidget {
body: Stack(
children: [
Positioned.fill(
- child: Row(
- children: [
- Expanded(
- child: content,
- ),
- if (supplemental != null) //
- _buildSupplementalPanel(),
- ],
- ),
+ child: _buildContent(),
),
if (overlay != null) //
Positioned.fill(
@@ -52,7 +44,31 @@ class InTheLabScaffold extends StatelessWidget {
);
}
- Widget _buildSupplementalPanel() {
+ Widget _buildContent() {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ if (constraints.maxWidth / constraints.maxHeight >= 1) {
+ return _buildContentForDesktop();
+ } else {
+ return _buildContentForMobile();
+ }
+ },
+ );
+ }
+
+ Widget _buildContentForDesktop() {
+ return Row(
+ children: [
+ Expanded(
+ child: content,
+ ),
+ if (supplemental != null) //
+ _buildSupplementalSidePanel(),
+ ],
+ );
+ }
+
+ Widget _buildSupplementalSidePanel() {
return Container(
width: 250,
height: double.infinity,
@@ -82,6 +98,58 @@ class InTheLabScaffold extends StatelessWidget {
),
);
}
+
+ Widget _buildContentForMobile() {
+ return SafeArea(
+ left: false,
+ right: false,
+ bottom: false,
+ child: Padding(
+ // Push the content down below the nav drawer menu button.
+ padding: const EdgeInsets.only(top: 24),
+ child: Column(
+ children: [
+ Expanded(
+ child: content,
+ ),
+ if (supplemental != null) //
+ _buildSupplementalBottomPanel(),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildSupplementalBottomPanel() {
+ return Container(
+ width: double.infinity,
+ height: 200,
+ decoration: BoxDecoration(
+ border: Border(top: BorderSide(color: Colors.white.withOpacity(0.1))),
+ ),
+ child: Stack(
+ children: [
+ Center(
+ child: Icon(
+ Icons.biotech,
+ color: Colors.white.withOpacity(0.05),
+ size: 84,
+ ),
+ ),
+ Positioned.fill(
+ child: Center(
+ child: SizedBox(
+ width: double.infinity,
+ child: SingleChildScrollView(
+ child: supplemental!,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
}
// Makes text light, for use during dark mode styling.
diff --git a/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart b/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart
index 07f6df7e0..0ca007697 100644
--- a/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart
+++ b/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart
@@ -76,6 +76,7 @@ class _SuperIOSTextFieldDemoState extends State {
maxLines: config.maxLines,
lineHeight: lineHeight,
textInputAction: TextInputAction.done,
+ popoverToolbarBuilder: iOSSystemPopoverTextFieldToolbarWithFallback,
showDebugPaint: config.showDebugPaint,
),
);
diff --git a/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart b/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart
new file mode 100644
index 000000000..873461833
--- /dev/null
+++ b/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart
@@ -0,0 +1,63 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(_FlutterTextFieldDemoApp());
+}
+
+class _FlutterTextFieldDemoApp extends StatelessWidget {
+ const _FlutterTextFieldDemoApp();
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ body: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Center(
+ child: _DemoTextField(),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _DemoTextField extends StatefulWidget {
+ const _DemoTextField();
+
+ @override
+ State<_DemoTextField> createState() => _DemoTextFieldState();
+}
+
+class _DemoTextFieldState extends State<_DemoTextField> {
+ final _textController = TextEditingController();
+
+ @override
+ void dispose() {
+ _textController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return TextField(
+ controller: _textController,
+ decoration: InputDecoration(
+ hintText: "Enter text...",
+ ),
+ contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
+ // If supported, show the system context menu.
+ if (SystemContextMenu.isSupported(context)) {
+ return SystemContextMenu.editableText(
+ editableTextState: editableTextState,
+ );
+ }
+ // Otherwise, show the flutter-rendered context menu for the current
+ // platform.
+ return AdaptiveTextSelectionToolbar.editableText(
+ editableTextState: editableTextState,
+ );
+ });
+ }
+}
diff --git a/super_editor/example/lib/main.dart b/super_editor/example/lib/main.dart
index a8435a378..0b1b12c3d 100644
--- a/super_editor/example/lib/main.dart
+++ b/super_editor/example/lib/main.dart
@@ -15,6 +15,7 @@ import 'package:example/demos/flutter_features/demo_inline_widgets.dart';
import 'package:example/demos/flutter_features/textinputclient/basic_text_input_client.dart';
import 'package:example/demos/flutter_features/textinputclient/textfield.dart';
import 'package:example/demos/in_the_lab/feature_action_tags.dart';
+import 'package:example/demos/in_the_lab/feature_ios_native_context_menu.dart';
import 'package:example/demos/in_the_lab/feature_pattern_tags.dart';
import 'package:example/demos/in_the_lab/feature_stable_tags.dart';
import 'package:example/demos/in_the_lab/selected_text_colors_demo.dart';
@@ -324,6 +325,13 @@ final _menu = <_MenuGroup>[
return const ActionTagsFeatureDemo();
},
),
+ _MenuItem(
+ icon: Icons.apple,
+ title: 'Native iOS Toolbar',
+ pageBuilder: (context) {
+ return const NativeIosContextMenuFeatureDemo();
+ },
+ ),
],
),
_MenuGroup(
diff --git a/super_editor/lib/src/default_editor/super_editor.dart b/super_editor/lib/src/default_editor/super_editor.dart
index 98c3a478d..ad907131d 100644
--- a/super_editor/lib/src/default_editor/super_editor.dart
+++ b/super_editor/lib/src/default_editor/super_editor.dart
@@ -38,6 +38,7 @@ import 'package:super_editor/src/infrastructure/render_sliver_ext.dart';
import 'package:super_text_layout/super_text_layout.dart';
import '../infrastructure/document_gestures_interaction_overrides.dart';
+import '../infrastructure/platforms/ios/ios_system_context_menu.dart';
import '../infrastructure/platforms/mobile_documents.dart';
import 'attributions.dart';
import 'blockquote.dart';
@@ -867,6 +868,42 @@ class SuperEditorState extends State {
}
}
+/// A [DocumentFloatingToolbarBuilder] that displays the iOS system popover toolbar, if the version of
+/// iOS is recent enough, otherwise builds [defaultIosEditorToolbarBuilder].
+Widget iOSSystemPopoverEditorToolbarWithFallbackBuilder(
+ BuildContext context,
+ Key floatingToolbarKey,
+ LeaderLink focalPoint,
+ CommonEditorOperations editorOps,
+ SuperEditorIosControlsController editorControlsController,
+) {
+ if (CurrentPlatform.isWeb) {
+ // On web, we defer to the browser's internal overlay controls for mobile.
+ return const SizedBox();
+ }
+
+ if (focalPoint.offset == null || focalPoint.leaderSize == null) {
+ // It's unclear when/why this might happen. But there seem to be some
+ // cases, such as placing a caret in an empty document and tapping again
+ // to show the toolbar.
+ return const SizedBox();
+ }
+
+ if (IOSSystemContextMenu.isSupported(context)) {
+ return IOSSystemContextMenu(
+ anchor: focalPoint.offset! & focalPoint.leaderSize!,
+ );
+ }
+
+ return defaultIosEditorToolbarBuilder(
+ context,
+ floatingToolbarKey,
+ focalPoint,
+ editorOps,
+ editorControlsController,
+ );
+}
+
/// Builds a standard editor-style iOS floating toolbar.
Widget defaultIosEditorToolbarBuilder(
BuildContext context,
diff --git a/super_editor/lib/src/infrastructure/platforms/ios/ios_system_context_menu.dart b/super_editor/lib/src/infrastructure/platforms/ios/ios_system_context_menu.dart
new file mode 100644
index 000000000..32c0a5af1
--- /dev/null
+++ b/super_editor/lib/src/infrastructure/platforms/ios/ios_system_context_menu.dart
@@ -0,0 +1,87 @@
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+
+/// Displays the iOS system context menu on top of the Flutter view.
+///
+/// This class was copied and adjusted from Flutter's [SystemContextMenu].
+///
+/// Currently, only supports iOS 16.0 and above.
+///
+/// The context menu is the menu that appears, for example, when doing text
+/// selection. Flutter typically draws this menu itself, but this class deals
+/// with the platform-rendered context menu instead.
+///
+/// There can only be one system context menu visible at a time. Building this
+/// widget when the system context menu is already visible will hide the old one
+/// and display this one. A system context menu that is hidden is informed via
+/// [onSystemHide].
+///
+/// To check if the current device supports showing the system context menu,
+/// call [isSupported].
+///
+/// See also:
+///
+/// * [SystemContextMenuController], which directly controls the hiding and
+/// showing of the system context menu.
+class IOSSystemContextMenu extends StatefulWidget {
+ /// Whether the current device supports showing the system context menu.
+ ///
+ /// Currently, this is only supported on iOS 16.0 and above.
+ static bool isSupported(BuildContext context) {
+ return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
+ }
+
+ const IOSSystemContextMenu({
+ super.key,
+ required this.anchor,
+ this.onSystemHide,
+ });
+
+ /// The [Rect] that the context menu should point to.
+ final Rect anchor;
+
+ /// Called when the system hides this context menu.
+ ///
+ /// For example, tapping outside of the context menu typically causes the
+ /// system to hide the menu.
+ ///
+ /// This is not called when showing a new system context menu causes another
+ /// to be hidden.
+ final VoidCallback? onSystemHide;
+
+ @override
+ State createState() => _IOSSystemContextMenuState();
+}
+
+class _IOSSystemContextMenuState extends State {
+ late final SystemContextMenuController _systemContextMenuController;
+
+ @override
+ void initState() {
+ super.initState();
+ _systemContextMenuController = SystemContextMenuController(
+ onSystemHide: widget.onSystemHide,
+ );
+ _systemContextMenuController.show(widget.anchor);
+ }
+
+ @override
+ void didUpdateWidget(IOSSystemContextMenu oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.anchor != oldWidget.anchor) {
+ _systemContextMenuController.show(widget.anchor);
+ }
+ }
+
+ @override
+ void dispose() {
+ _systemContextMenuController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ assert(IOSSystemContextMenu.isSupported(context));
+ return const SizedBox.shrink();
+ }
+}
diff --git a/super_editor/lib/src/super_textfield/ios/_caret.dart b/super_editor/lib/src/super_textfield/ios/caret.dart
similarity index 100%
rename from super_editor/lib/src/super_textfield/ios/_caret.dart
rename to super_editor/lib/src/super_textfield/ios/caret.dart
diff --git a/super_editor/lib/src/super_textfield/ios/_editing_controls.dart b/super_editor/lib/src/super_textfield/ios/editing_controls.dart
similarity index 100%
rename from super_editor/lib/src/super_textfield/ios/_editing_controls.dart
rename to super_editor/lib/src/super_textfield/ios/editing_controls.dart
diff --git a/super_editor/lib/src/super_textfield/ios/_floating_cursor.dart b/super_editor/lib/src/super_textfield/ios/floating_cursor.dart
similarity index 100%
rename from super_editor/lib/src/super_textfield/ios/_floating_cursor.dart
rename to super_editor/lib/src/super_textfield/ios/floating_cursor.dart
diff --git a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart
index 1a85b2bf1..60b43fdbe 100644
--- a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart
+++ b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart
@@ -15,17 +15,20 @@ import 'package:super_editor/src/super_textfield/infrastructure/fill_width_if_co
import 'package:super_editor/src/super_textfield/infrastructure/hint_text.dart';
import 'package:super_editor/src/super_textfield/infrastructure/text_scrollview.dart';
import 'package:super_editor/src/super_textfield/input_method_engine/_ime_text_editing_controller.dart';
-import 'package:super_editor/src/super_textfield/ios/_editing_controls.dart';
+import 'package:super_editor/src/super_textfield/ios/editing_controls.dart';
import 'package:super_text_layout/super_text_layout.dart';
import '../metrics.dart';
import '../styles.dart';
-import '_floating_cursor.dart';
-import '_user_interaction.dart';
+import 'floating_cursor.dart';
+import '../../infrastructure/platforms/ios/ios_system_context_menu.dart';
+import 'user_interaction.dart';
export '../infrastructure/magnifier.dart';
-export '_caret.dart';
-export '_user_interaction.dart';
+export 'caret.dart';
+export 'editing_controls.dart';
+export '../../infrastructure/platforms/ios/ios_system_context_menu.dart';
+export 'user_interaction.dart';
final _log = iosTextFieldLog;
@@ -50,7 +53,7 @@ class SuperIOSTextField extends StatefulWidget {
this.textInputAction,
this.imeConfiguration,
this.showComposingUnderline = true,
- this.popoverToolbarBuilder = _defaultPopoverToolbarBuilder,
+ this.popoverToolbarBuilder = defaultIosPopoverToolbarBuilder,
this.showDebugPaint = false,
}) : super(key: key);
@@ -149,7 +152,7 @@ class SuperIOSTextField extends StatefulWidget {
final bool showComposingUnderline;
/// Builder that creates the popover toolbar widget that appears when text is selected.
- final Widget Function(BuildContext, IOSEditingOverlayController) popoverToolbarBuilder;
+ final IOSPopoverToolbarBuilder popoverToolbarBuilder;
/// Whether to paint debug guides.
final bool showDebugPaint;
@@ -694,7 +697,23 @@ class SuperIOSTextFieldState extends State
}
}
-Widget _defaultPopoverToolbarBuilder(BuildContext context, IOSEditingOverlayController controller) {
+/// Builder that returns a widget for an iOS-style popover editing toolbar.
+typedef IOSPopoverToolbarBuilder = Widget Function(BuildContext, IOSEditingOverlayController);
+
+/// An [IOSPopoverToolbarBuilder] that displays the iOS system popover toolbar, if the version of
+/// iOS is recent enough, otherwise builds [defaultIosPopoverToolbarBuilder].
+Widget iOSSystemPopoverTextFieldToolbarWithFallback(BuildContext context, IOSEditingOverlayController controller) {
+ if (IOSSystemContextMenu.isSupported(context)) {
+ return IOSSystemContextMenu(
+ anchor: controller.toolbarFocalPoint.offset! & controller.toolbarFocalPoint.leaderSize!,
+ );
+ }
+
+ return defaultIosPopoverToolbarBuilder(context, controller);
+}
+
+/// Returns a widget for the default/standard iOS-style popover provided by Super Text Field.
+Widget defaultIosPopoverToolbarBuilder(BuildContext context, IOSEditingOverlayController controller) {
return IOSTextEditingFloatingToolbar(
focalPoint: controller.toolbarFocalPoint,
onCutPressed: () {
diff --git a/super_editor/lib/src/super_textfield/ios/_user_interaction.dart b/super_editor/lib/src/super_textfield/ios/user_interaction.dart
similarity index 99%
rename from super_editor/lib/src/super_textfield/ios/_user_interaction.dart
rename to super_editor/lib/src/super_textfield/ios/user_interaction.dart
index be66c761d..e5963bed4 100644
--- a/super_editor/lib/src/super_textfield/ios/_user_interaction.dart
+++ b/super_editor/lib/src/super_textfield/ios/user_interaction.dart
@@ -10,7 +10,7 @@ import 'package:super_editor/src/super_textfield/super_textfield.dart';
import 'package:super_editor/src/test/test_globals.dart';
import 'package:super_text_layout/super_text_layout.dart';
-import '_editing_controls.dart';
+import 'editing_controls.dart';
final _log = iosTextFieldLog;
diff --git a/super_editor/lib/super_editor.dart b/super_editor/lib/super_editor.dart
index 26f6f9dc0..0cb39bee3 100644
--- a/super_editor/lib/super_editor.dart
+++ b/super_editor/lib/super_editor.dart
@@ -75,6 +75,7 @@ export 'src/infrastructure/flutter/text_selection.dart';
export 'src/infrastructure/platforms/android/android_document_controls.dart';
export 'src/infrastructure/platforms/android/toolbar.dart';
export 'src/infrastructure/platforms/ios/ios_document_controls.dart';
+export 'src/infrastructure/platforms/ios/ios_system_context_menu.dart';
export 'src/infrastructure/platforms/ios/floating_cursor.dart';
export 'src/infrastructure/platforms/ios/toolbar.dart';
export 'src/infrastructure/platforms/ios/magnifier.dart';