diff --git a/lib/screens/common_widgets/env_trigger_field.dart b/lib/screens/common_widgets/env_trigger_field.dart index a7e470e76..d5193f1f2 100644 --- a/lib/screens/common_widgets/env_trigger_field.dart +++ b/lib/screens/common_widgets/env_trigger_field.dart @@ -41,20 +41,29 @@ class EnvironmentTriggerField extends StatefulWidget { class EnvironmentTriggerFieldState extends State { late TextEditingController controller; late FocusNode _focusNode; + bool _isFocused = false; @override void initState() { super.initState(); controller = widget.controller ?? TextEditingController.fromValue(TextEditingValue( - text: widget.initialValue!, - selection: - TextSelection.collapsed(offset: widget.initialValue!.length))); + text: widget.initialValue ?? '', + selection: TextSelection.collapsed( + offset: widget.initialValue?.length ?? 0))); _focusNode = widget.focusNode ?? FocusNode(); + _focusNode.addListener(_handleFocusChange); + } + + void _handleFocusChange() { + setState(() { + _isFocused = _focusNode.hasFocus; + }); } @override void dispose() { + _focusNode.removeListener(_handleFocusChange); controller.dispose(); _focusNode.dispose(); super.dispose(); @@ -67,9 +76,9 @@ class EnvironmentTriggerFieldState extends State { (oldWidget.initialValue != widget.initialValue)) { controller = widget.controller ?? TextEditingController.fromValue(TextEditingValue( - text: widget.initialValue!, + text: widget.initialValue ?? '', selection: TextSelection.collapsed( - offset: widget.initialValue!.length))); + offset: widget.initialValue?.length ?? 0))); } } @@ -114,19 +123,25 @@ class EnvironmentTriggerFieldState extends State { }), ], fieldViewBuilder: (context, textEditingController, focusnode) { - return ExtendedTextField( - controller: textEditingController, - focusNode: focusnode, - decoration: widget.decoration, - style: widget.style, - onChanged: widget.onChanged, - onSubmitted: widget.onFieldSubmitted, - specialTextSpanBuilder: EnvRegExpSpanBuilder(), - onTapOutside: (event) { - _focusNode.unfocus(); - }, + return AnimatedSize( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: ExtendedTextField( + controller: textEditingController, + focusNode: focusnode, + maxLines: _isFocused ? null : 1, + minLines: 1, + decoration: widget.decoration, + style: widget.style, + onChanged: widget.onChanged, + onSubmitted: widget.onFieldSubmitted, + specialTextSpanBuilder: EnvRegExpSpanBuilder(), + onTapOutside: (event) { + _focusNode.unfocus(); + }, + ), ); }, ); } -} +} \ No newline at end of file diff --git a/lib/screens/envvar/editor_pane/variables_pane.dart b/lib/screens/envvar/editor_pane/variables_pane.dart index f6172ec75..b6228238e 100644 --- a/lib/screens/envvar/editor_pane/variables_pane.dart +++ b/lib/screens/envvar/editor_pane/variables_pane.dart @@ -23,6 +23,8 @@ class EditEnvironmentVariablesState final random = Random.secure(); late List variableRows; bool isAddingRow = false; + OverlayEntry? _overlayEntry; + FocusNode? _currentFocusNode; @override void initState() { @@ -30,6 +32,93 @@ class EditEnvironmentVariablesState seed = random.nextInt(kRandMax); } + @override + void dispose() { + _removeOverlay(); + super.dispose(); + } + + void _removeOverlay() { + _overlayEntry?.remove(); + _overlayEntry = null; + _currentFocusNode?.removeListener(_handleOverlayFocusChange); + _currentFocusNode = null; + } + + void _handleOverlayFocusChange() { + if (_currentFocusNode != null && !_currentFocusNode!.hasFocus) { + _removeOverlay(); + } + } + + void _showOverlay( + GlobalKey key, + String text, + TextStyle textStyle, + ColorScheme clrScheme, + FocusNode focusNode, + TextEditingController controller, + InputDecoration decoration, + ) { + _removeOverlay(); + + final RenderBox renderBox = key.currentContext!.findRenderObject() as RenderBox; + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + + _currentFocusNode = focusNode; + _currentFocusNode?.addListener(_handleOverlayFocusChange); + + _overlayEntry = OverlayEntry( + builder: (context) => Positioned( + left: position.dx, + top: position.dy, + width: size.width, + child: Material( + elevation: 8, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: clrScheme.surface, + ), + child: TextField( + controller: controller, + focusNode: focusNode, + style: textStyle, + decoration: decoration, + maxLines: null, + keyboardType: TextInputType.multiline, + autofocus: true, + onSubmitted: (_) { + focusNode.unfocus(); + _removeOverlay(); + }, + ), + ), + ), + ), + ); + + Overlay.of(context).insert(_overlayEntry!); + } + + void _onOverlayToggle( + bool show, + GlobalKey key, + String text, + TextStyle textStyle, + ColorScheme clrScheme, + FocusNode focusNode, + TextEditingController controller, + InputDecoration decoration, + ) { + if (show) { + _showOverlay(key, text, textStyle, clrScheme, focusNode, controller, decoration); + } else { + _removeOverlay(); + } + } + void _onFieldChange(String selectedId) { final environment = ref.read(selectedEnvironmentModelProvider); final secrets = getEnvironmentSecrets(environment); @@ -67,7 +156,7 @@ class EditEnvironmentVariablesState fixedWidth: 30, ), DataColumn2( - label: Text("Variable value"), + label: Text(" jars value"), ), DataColumn2( label: Text(''), @@ -118,6 +207,7 @@ class EditEnvironmentVariablesState _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, + onOverlayToggle: _onOverlayToggle, ), ), DataCell( @@ -146,6 +236,7 @@ class EditEnvironmentVariablesState _onFieldChange(selectedId!); }, colorScheme: Theme.of(context).colorScheme, + onOverlayToggle: _onOverlayToggle, ), ), DataCell( @@ -175,57 +266,63 @@ class EditEnvironmentVariablesState }, ); - return Stack( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: kBorderRadius12, - ), - margin: kPh10t10, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Theme( - data: Theme.of(context) - .copyWith(scrollbarTheme: kDataTableScrollbarTheme), - child: DataTable2( - columnSpacing: 12, - dividerThickness: 0, - horizontalMargin: 0, - headingRowHeight: 0, - dataRowHeight: kDataTableRowHeight, - bottomMargin: kDataTableBottomPadding, - isVerticalScrollBarVisible: true, - columns: columns, - rows: dataRows, + return GestureDetector( + onTap: () { + FocusScope.of(context).requestFocus(FocusNode()); + _removeOverlay(); + }, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: kBorderRadius12, + ), + margin: kPh10t10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Theme( + data: Theme.of(context) + .copyWith(scrollbarTheme: kDataTableScrollbarTheme), + child: DataTable2( + columnSpacing: 12, + dividerThickness: 0, + horizontalMargin: 0, + headingRowHeight: 0, + dataRowHeight: null, + bottomMargin: kDataTableBottomPadding, + isVerticalScrollBarVisible: true, + columns: columns, + rows: dataRows, + ), ), ), - ), - if (!kIsMobile) kVSpacer40, - ], + if (!kIsMobile) kVSpacer40, + ], + ), ), - ), - if (!kIsMobile) - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: kPb15, - child: ElevatedButton.icon( - onPressed: () { - variableRows.add(kEnvironmentVariableEmptyModel); - _onFieldChange(selectedId!); - }, - icon: const Icon(Icons.add), - label: const Text( - kLabelAddVariable, - style: kTextStyleButton, + if (!kIsMobile) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: kPb15, + child: ElevatedButton.icon( + onPressed: () { + variableRows.add(kEnvironmentVariableEmptyModel); + _onFieldChange(selectedId!); + }, + icon: const Icon(Icons.add), + label: const Text( + kLabelAddVariable, + style: kTextStyleButton, + ), ), ), ), - ), - ], + ], + ), ); } -} +} \ No newline at end of file diff --git a/lib/widgets/field_cell.dart b/lib/widgets/field_cell.dart index 278de67f7..588592f75 100644 --- a/lib/widgets/field_cell.dart +++ b/lib/widgets/field_cell.dart @@ -9,6 +9,7 @@ class CellField extends StatelessWidget { this.hintText, this.onChanged, this.colorScheme, + this.onOverlayToggle, }); final String keyId; @@ -16,16 +17,31 @@ class CellField extends StatelessWidget { final String? hintText; final void Function(String)? onChanged; final ColorScheme? colorScheme; + final void Function( + bool, + GlobalKey>, + String, + TextStyle, + ColorScheme, + FocusNode, + TextEditingController, + InputDecoration, + )? onOverlayToggle; @override Widget build(BuildContext context) { - return ADOutlinedTextField( - keyId: keyId, - initialValue: initialValue, - hintText: hintText, - hintTextFontSize: Theme.of(context).textTheme.bodySmall?.fontSize, - onChanged: onChanged, - colorScheme: colorScheme, + return SizedBox( + width: double.infinity, + child: ADOutlinedTextField( + keyId: keyId, + initialValue: initialValue, + hintText: hintText, + hintTextFontSize: Theme.of(context).textTheme.bodySmall?.fontSize, + onChanged: onChanged, + onOverlayToggle: onOverlayToggle, + colorScheme: colorScheme, + isDense: true, + ), ); } -} +} \ No newline at end of file diff --git a/packages/apidash_design_system/lib/widgets/textfield_outlined.dart b/packages/apidash_design_system/lib/widgets/textfield_outlined.dart index b9b2588d2..f5db007b9 100644 --- a/packages/apidash_design_system/lib/widgets/textfield_outlined.dart +++ b/packages/apidash_design_system/lib/widgets/textfield_outlined.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import '../tokens/tokens.dart'; import 'decoration_input_textfield.dart'; -class ADOutlinedTextField extends StatelessWidget { +class ADOutlinedTextField extends StatefulWidget { const ADOutlinedTextField({ super.key, this.keyId, @@ -26,6 +27,7 @@ class ADOutlinedTextField extends StatelessWidget { this.isDense, this.onChanged, this.colorScheme, + this.onOverlayToggle, }); final String? keyId; @@ -49,36 +51,211 @@ class ADOutlinedTextField extends StatelessWidget { final Color? enabledBorderColor; final void Function(String)? onChanged; final ColorScheme? colorScheme; + final void Function( + bool, + GlobalKey, + String, + TextStyle, + ColorScheme, + FocusNode, + TextEditingController, + InputDecoration, + )? onOverlayToggle; + + @override + State createState() => _ADOutlinedTextFieldState(); +} + +class _ADOutlinedTextFieldState extends State { + late FocusNode _focusNode; + late TextEditingController _controller; + bool _isFocused = false; + bool _isMultiLine = false; + final GlobalKey _textFieldKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _controller = widget.controller ?? TextEditingController(text: widget.initialValue); + _focusNode.addListener(_handleFocusChange); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkMultiLineAndUpdateOverlay(); + }); + } + + void _handleFocusChange() { + final wasFocused = _isFocused; + _isFocused = _focusNode.hasFocus; + + if (_isFocused != wasFocused) { + setState(() {}); + _checkMultiLineAndUpdateOverlay(); + } + + if (!_isFocused && widget.onOverlayToggle != null) { + widget.onOverlayToggle!( + false, + _textFieldKey, + _controller.text, + TextStyle(), + Theme.of(context).colorScheme, + _focusNode, + _controller, + InputDecoration(), + ); + } + } + + @override + void dispose() { + widget.onOverlayToggle?.call( + false, + _textFieldKey, + '', + TextStyle(), + Theme.of(context).colorScheme, + _focusNode, + _controller, + InputDecoration(), + ); + if (widget.controller == null) { + _controller.dispose(); + } + _focusNode.removeListener(_handleFocusChange); + _focusNode.dispose(); + super.dispose(); + } + + int _calculateLineCount(String text, TextStyle textStyle, double maxWidth) { + final textPainter = TextPainter( + text: TextSpan(text: text, style: textStyle), + textDirection: TextDirection.ltr, + maxLines: null, + ); + textPainter.layout(maxWidth: maxWidth); + return textPainter.computeLineMetrics().length; + } + + void _checkMultiLineAndUpdateOverlay() { + var clrScheme = widget.colorScheme ?? Theme.of(context).colorScheme; + final textStyle = widget.textStyle ?? + kCodeStyle.copyWith( + fontSize: widget.textFontSize, + color: widget.textColor ?? clrScheme.onSurface, + ); + final contentPadding = widget.contentPadding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8); + final horizontalPadding = contentPadding is EdgeInsets ? contentPadding.horizontal : 16.0; + + double maxWidth = double.infinity; + if (_textFieldKey.currentContext != null) { + final renderBox = _textFieldKey.currentContext!.findRenderObject() as RenderBox?; + if (renderBox != null && renderBox.hasSize) { + maxWidth = renderBox.size.width - horizontalPadding; + } + } + + final text = _controller.text.isEmpty ? widget.hintText ?? '' : _controller.text; + final lineCount = _calculateLineCount(text, textStyle, maxWidth); + final isMultiLine = lineCount > 1; + + _isMultiLine = isMultiLine; + + final showOverlay = _isFocused && _isMultiLine; + + final decoration = getTextFieldInputDecoration( + clrScheme, + fillColor: widget.fillColor, + hintText: widget.hintText, + hintTextStyle: widget.hintTextStyle, + hintTextFontSize: widget.hintTextFontSize, + hintTextColor: widget.hintTextColor, + contentPadding: contentPadding, + focussedBorderColor: widget.focussedBorderColor, + enabledBorderColor: widget.enabledBorderColor, + isDense: widget.isDense ?? true, + ); + + widget.onOverlayToggle?.call( + showOverlay, + _textFieldKey, + text, + textStyle, + clrScheme, + _focusNode, + _controller, + decoration, + ); + } @override Widget build(BuildContext context) { - var clrScheme = colorScheme ?? Theme.of(context).colorScheme; - return TextFormField( - key: keyId != null ? Key(keyId!) : null, - controller: controller, - readOnly: readOnly, - enabled: enabled, - maxLines: maxLines, - expands: expands, - initialValue: initialValue, - style: textStyle ?? - kCodeStyle.copyWith( - fontSize: textFontSize, - color: textColor ?? clrScheme.onSurface, - ), - decoration: getTextFieldInputDecoration( - clrScheme, - fillColor: fillColor, - hintText: hintText, - hintTextStyle: hintTextStyle, - hintTextFontSize: hintTextFontSize, - hintTextColor: hintTextColor, - contentPadding: contentPadding, - focussedBorderColor: focussedBorderColor, - enabledBorderColor: enabledBorderColor, - isDense: isDense, + var clrScheme = widget.colorScheme ?? Theme.of(context).colorScheme; + final textStyle = widget.textStyle ?? + kCodeStyle.copyWith( + fontSize: widget.textFontSize, + color: widget.textColor ?? clrScheme.onSurface, + ); + final contentPadding = widget.contentPadding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8); + + return RawKeyboardListener( + focusNode: FocusNode(), + onKey: (RawKeyEvent event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) { + _focusNode.unfocus(); + widget.onChanged?.call(_controller.text); + widget.onOverlayToggle?.call( + false, + _textFieldKey, + _controller.text, + textStyle, + clrScheme, + _focusNode, + _controller, + InputDecoration(), + ); + } + }, + child: TextFormField( + key: _textFieldKey, + controller: _controller, + focusNode: _focusNode, + readOnly: widget.readOnly, + enabled: widget.enabled, + maxLines: _isFocused ? null : 1, + keyboardType: TextInputType.multiline, + expands: widget.expands, + style: textStyle, + decoration: getTextFieldInputDecoration( + clrScheme, + fillColor: widget.fillColor, + hintText: widget.hintText, + hintTextStyle: widget.hintTextStyle, + hintTextFontSize: widget.hintTextFontSize, + hintTextColor: widget.hintTextColor, + contentPadding: contentPadding, + focussedBorderColor: widget.focussedBorderColor, + enabledBorderColor: widget.enabledBorderColor, + isDense: widget.isDense ?? true, + ), + onChanged: (value) { + widget.onChanged?.call(value); + _checkMultiLineAndUpdateOverlay(); + }, + onTap: () { + if (!_isFocused) { + FocusScope.of(context).requestFocus(FocusNode()); + _focusNode.requestFocus(); + _isFocused = true; + setState(() {}); + } + _checkMultiLineAndUpdateOverlay(); + }, + onFieldSubmitted: (_) { + _focusNode.unfocus(); + widget.onChanged?.call(_controller.text); + }, ), - onChanged: onChanged, ); } -} +} \ No newline at end of file diff --git a/packages/apidash_design_system/lib/widgets/textfield_raw.dart b/packages/apidash_design_system/lib/widgets/textfield_raw.dart index a58964178..366bcbcd6 100644 --- a/packages/apidash_design_system/lib/widgets/textfield_raw.dart +++ b/packages/apidash_design_system/lib/widgets/textfield_raw.dart @@ -1,7 +1,7 @@ import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; -class ADRawTextField extends StatelessWidget { +class ADRawTextField extends StatefulWidget { const ADRawTextField({ super.key, this.onChanged, @@ -19,23 +19,50 @@ class ADRawTextField extends StatelessWidget { final TextStyle? style; final bool readOnly; + @override + State createState() => _ADRawTextFieldState(); +} + +class _ADRawTextFieldState extends State { + bool isFocused = false; + @override + void initState() { + isFocused = false; + super.initState(); + } + + void alterFocus(bool val) { + setState(() { + isFocused = val; + }); + } + @override Widget build(BuildContext context) { - return TextField( - readOnly: readOnly, - controller: controller, - onChanged: onChanged, - style: style, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: hintText, - hintStyle: hintTextStyle, - contentPadding: kPv8, + return AnimatedSize( + duration: Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: TextField( + onTap: () => alterFocus(true), + readOnly: widget.readOnly, + controller: widget.controller, + onSubmitted: (value) => alterFocus(false), + maxLines: isFocused ? null : 1, + minLines: 1, + onChanged: widget.onChanged, + style: widget.style, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: widget.hintText, + hintStyle: widget.hintTextStyle, + contentPadding: kPv8, + ), + onTapOutside: (PointerDownEvent event) { + FocusManager.instance.primaryFocus?.unfocus(); + alterFocus(false); + }, ), - onTapOutside: (PointerDownEvent event) { - FocusManager.instance.primaryFocus?.unfocus(); - }, ); } }