fix(ui): improve cross-platform text input behavior (#1477)

This commit is contained in:
Levi authored and GitHub committed 2026-09-06 17:18:57 +08:00
1 parent 16f2d30476
commit d511e66d56
19 files changed
+305 -28

No files matched your search

@@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../theme/app_design_tokens.dart';
import '../theme/app_palette.dart';
import '../../l10n/l10n.dart';
import 'app_text_field.dart';
class AppHttpHeadersController extends ChangeNotifier {
AppHttpHeadersController({Map<String, String>? headers, Iterable<String> defaultNames = const []}) {
@@ -171,7 +172,7 @@ class _HeaderTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return shad.TextField(
return AppTextField(
controller: controller,
hintText: hintText,
filled: true,
@@ -38,7 +38,7 @@ class AppLoadingButton extends StatelessWidget {
children: [
leading,
const SizedBox(width: AppDesignTokens.space8),
effectiveChild,
Flexible(child: effectiveChild),
],
);
final effectiveOnPressed = loading ? null : onPressed;
@@ -2,6 +2,8 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import 'app_text_field.dart';
/// A bounded numeric text field with the app's standard spinner controls.
class AppNumberInput extends StatefulWidget {
const AppNumberInput({
@@ -80,7 +82,7 @@ class _AppNumberInputState extends State<AppNumberInput> {
FilteringTextInputFormatter.digitsOnly,
_NumericalRangeFormatter(min: widget.min.toInt(), max: widget.max?.toInt()),
];
return shad.TextField(
return AppTextField(
key: widget.fieldKey,
controller: widget.controller,
hintText: widget.hintText,
@@ -4,6 +4,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../../core/utils/breakpoints.dart';
import '../services/download_directory_picker.dart';
import 'app_text_field.dart';
import 'app_tooltip.dart';
enum AppPathPickerButtonStyle { ghost, outline }
@@ -119,7 +120,7 @@ class _AppPathPickerFieldState extends State<AppPathPickerField> {
onEnter: (_) => _syncTooltipPath(),
child: AppTooltip(
message: _tooltipPath,
child: shad.TextField(
child: AppTextField(
key: widget.fieldKey,
controller: widget.controller,
hintText: widget.hintText,
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart' as material show AdaptiveTextSelectionToolbar;
import 'package:flutter/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
/// The app-wide text field.
///
/// shadcn_flutter's custom context menu is rendered in a full-screen overlay
/// that can cover the app with an opaque surface. Use Flutter's adaptive
/// platform toolbar everywhere so long-press and right-click text actions have
/// consistent native behavior on mobile, desktop, and web.
class AppTextField extends shad.TextField {
const AppTextField({
super.key,
super.groupId,
super.controller,
super.initialValue,
super.focusNode,
super.undoController,
super.decoration,
super.padding,
super.placeholder,
super.crossAxisAlignment,
super.clearButtonSemanticLabel,
super.keyboardType,
super.textInputAction,
super.textCapitalization,
super.style,
super.strutStyle,
super.textAlign,
super.textAlignVertical,
super.textDirection,
super.readOnly,
super.showCursor,
super.autofocus,
super.obscuringCharacter,
super.obscureText,
super.autocorrect,
super.smartDashesType,
super.smartQuotesType,
super.enableSuggestions,
super.maxLines,
super.minLines,
super.expands,
super.maxLength,
super.maxLengthEnforcement,
super.onChanged,
super.onEditingComplete,
super.onSubmitted,
super.onTapOutside,
super.onTapUpOutside,
super.inputFormatters,
super.enabled,
super.cursorWidth,
super.cursorHeight,
super.cursorRadius,
super.cursorOpacityAnimates,
super.cursorColor,
super.selectionHeightStyle,
super.selectionWidthStyle,
super.keyboardAppearance,
super.scrollPadding,
super.enableInteractiveSelection,
super.selectionControls,
super.dragStartBehavior,
super.scrollController,
super.scrollPhysics,
super.onTap,
super.autofillHints,
super.clipBehavior,
super.restorationId,
super.stylusHandwritingEnabled,
super.enableIMEPersonalizedLearning,
super.contentInsertionConfiguration,
super.contextMenuBuilder = appTextFieldContextMenuBuilder,
super.hintText,
super.border,
super.borderRadius,
super.filled,
super.statesController,
super.magnifierConfiguration,
super.spellCheckConfiguration,
super.features,
super.submitFormatters,
super.skipInputFeatureFocusTraversal,
super.onDragSelectionStart,
super.onDragSelectionUpdate,
super.onDragSelectionEnd,
});
}
Widget appTextFieldContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
// Match Flutter's platform text-menu policy without falling back to
// shadcn_flutter's full-screen overlay. EditableText bypasses this builder
// on Web while the browser context menu is enabled.
if (SystemContextMenu.isSupportedByField(editableTextState)) {
return SystemContextMenu.editableText(editableTextState: editableTextState);
}
return material.AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState);
}