syncfusion / flutter-widgets

Syncfusion Flutter widgets libraries include high quality UI widgets and file-format packages to help you create rich, high-quality applications for iOS, Android, and web from a single code base.
1.6k stars 787 forks source link

syncfusion flutter datagrid Auto Focus on Startup and Up Down on Arrow Keys. #2134

Closed abdulrehmananwar closed 3 weeks ago

abdulrehmananwar commented 1 month ago

Use case

import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart';

class TextFieldAndDataGrid extends StatefulWidget { @override _TextFieldAndDataGridState createState() => _TextFieldAndDataGridState(); }

class _TextFieldAndDataGridState extends State { final TextEditingController _textController = TextEditingController(); final FocusNode _dataGridFocusNode = FocusNode(); // FocusNode for DataGrid final DataGridController _dataGridController = DataGridController(); List _employees = []; int _currentSelectedIndex = 0;

@override void initState() { super.initState();

// Initialize employees data with 30 rows
_employees = getEmployees();

// Set default selected index and focus on the DataGrid
WidgetsBinding.instance.addPostFrameCallback((_) {
  _dataGridController.selectedIndex = _currentSelectedIndex;
  _dataGridController.scrollToRow(_currentSelectedIndex.toDouble());
  _dataGridFocusNode.requestFocus();  // Focus on DataGrid when screen loads
});

}

@override void dispose() { _textController.dispose(); _dataGridFocusNode.dispose(); _dataGridController.dispose(); super.dispose(); }

@override Widget build(BuildContext context) { return Scaffold( body: RawKeyboardListener( focusNode: _dataGridFocusNode, // Focus on DataGrid for keyboard events onKey: (RawKeyEvent event) { if (event is RawKeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.arrowDown) { // Move to the next row if possible if (_currentSelectedIndex < _employees.length - 1) { _currentSelectedIndex++; _dataGridController.selectedIndex = _currentSelectedIndex; _dataGridController.scrollToRow(_currentSelectedIndex.toDouble()); _updateTextField('ArrowDown'); // Reflect keypress in TextField } } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { // Move to the previous row if possible if (_currentSelectedIndex > 0) { _currentSelectedIndex--; _dataGridController.selectedIndex = _currentSelectedIndex; _dataGridController.scrollToRow(_currentSelectedIndex.toDouble()); _updateTextField('ArrowUp'); // Reflect keypress in TextField } } else { // For any other key pressed, reflect the key in the TextField _updateTextField(event.logicalKey.keyLabel); } } }, child: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _textController, decoration: InputDecoration(labelText: 'Keypresses will show here'), readOnly: true, // Make the TextField read-only so we control input ), ), Expanded( child: SfDataGrid( source: EmployeeDataSource(_employees), controller: _dataGridController, selectionMode: SelectionMode.single, columns: [ GridColumn( columnName: 'id', label: Container( padding: EdgeInsets.all(8.0), alignment: Alignment.center, child: Text('ID'), ), ), GridColumn( columnName: 'name', label: Container( padding: EdgeInsets.all(8.0), alignment: Alignment.center, child: Text('Name'), ), ), GridColumn( columnName: 'designation', label: Container( padding: EdgeInsets.all(8.0), alignment: Alignment.center, child: Text('Designation'), ), ), ], ), ), ], ), ), ); }

// Update the TextField with the key pressed void _updateTextField(String key) { setState(() { _textController.text = key; }); } }

// Data model for the Employee class Employee { Employee(this.id, this.name, this.designation); final int id; final String name; final String designation; }

// DataSource class for DataGrid class EmployeeDataSource extends DataGridSource { EmployeeDataSource(this.employees) { dataGridRows = employees .map((e) => DataGridRow(cells: [ DataGridCell(columnName: 'id', value: e.id), DataGridCell(columnName: 'name', value: e.name), DataGridCell(columnName: 'designation', value: e.designation), ])) .toList(); }

List dataGridRows = []; List employees = [];

@override List get rows => dataGridRows;

@override DataGridRowAdapter buildRow(DataGridRow row) { return DataGridRowAdapter( cells: row.getCells().map((dataGridCell) { return Container( alignment: Alignment.center, padding: EdgeInsets.all(8.0), child: Text(dataGridCell.value.toString()), ); }).toList(), ); } }

// Sample data for employees (30 rows) List getEmployees() { return List.generate(30, (index) => Employee(index + 1, 'Employee $index', 'Designation $index')); }

Proposal

https://github.com/user-attachments/assets/e1db6017-63bc-4446-877e-b7a6b3838fb5

i want basically same pattren like when dialog is open there is one textfield and one gridview what is want is 1- when dialog open default datagridview first row selected. also which key i press it inputing the data into textfield 2- when i click arrow up or arrow down same time my datagridview row focus is moving 3- it also scrolling if arrow is going doing and same time if i press any keyboard keyword same time its also typing and filtering.

abineshPalanisamy commented 1 month ago

Hi @abdulrehmananwar ,

Based on the details provided, we have modified the sample to meet your expectations. In the updated sample, when you press the up or down arrow keys, the focus remains on the DataGrid, and scrolling occurs according to the current selection. Additionally, the TextField has been implemented to facilitate filtering within the DataGrid.

We’ve included a modified sample for your reference. Please review it for further details.

Regards, Abinesh P

abdulrehmananwar commented 1 month ago

import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart';

void main() { runApp(const MaterialApp( home: MyHomePage(), )); }

class MyHomePage extends StatelessWidget { const MyHomePage({super.key});

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Dialog with DataGrid'), ), body: Center( child: ElevatedButton( onPressed: () { showDialog( context: context, builder: (context) { return const DataGridDialog(); }, ); }, child: const Text('Open Dialog'), ), ), ); } }

class DataGridDialog extends StatefulWidget { const DataGridDialog({super.key});

@override _DataGridDialogState createState() => _DataGridDialogState(); }

class _DataGridDialogState extends State { final TextEditingController _textController = TextEditingController(); final FocusNode _dataGridFocusNode = FocusNode(); final FocusNode _textFieldFocusNode = FocusNode(); final DataGridController _dataGridController = DataGridController(); late EmployeeDataSource employeeDataSource; List _employees = []; int _currentSelectedIndex = 0;

@override void initState() { super.initState(); _employees = getEmployees(); employeeDataSource = EmployeeDataSource(_employees);

// Set default selected index and focus on the DataGrid and TextField when dialog opens.
WidgetsBinding.instance.addPostFrameCallback((_) {
  _dataGridController.selectedIndex = _currentSelectedIndex;
  _dataGridController.scrollToRow(_currentSelectedIndex.toDouble());
  _textFieldFocusNode.requestFocus(); // Focus on the TextField
});

}

@override void dispose() { _textController.dispose(); _dataGridFocusNode.dispose(); _textFieldFocusNode.dispose(); _dataGridController.dispose(); super.dispose(); }

void _handleKeyEvent(RawKeyEvent event) { if (event is RawKeyDownEvent) { // Only intercept arrow keys while TextField is focused if (_textFieldFocusNode.hasFocus) { if (event.logicalKey == LogicalKeyboardKey.arrowDown) { _moveSelection(1); } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { // Move the cursor to the end of the TextField when up arrow is pressed print('going cursor to last'); // Move the cursor to the end of the TextField when up arrow is pressed Future.delayed(Duration(milliseconds: 5), () { _textController.selection = TextSelection.fromPosition( TextPosition(offset: _textController.text.length), ); }); _moveSelection(-1); } } } }

void _moveSelection(int offset) { setState(() { _currentSelectedIndex += offset; _currentSelectedIndex = _currentSelectedIndex.clamp(0, _employees.length - 1); _dataGridController.selectedIndex = _currentSelectedIndex; _dataGridController.scrollToRow(_currentSelectedIndex.toDouble()); }); }

@override Widget build(BuildContext context) { return AlertDialog( title: const Text('DataGrid Dialog'), content: SizedBox( width: 600, height: 400, child: RawKeyboardListener( focusNode: _dataGridFocusNode, onKey: _handleKeyEvent, child: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _textController, focusNode: _textFieldFocusNode, decoration: const InputDecoration(labelText: 'Type to filter...'), onChanged: (value) { // Apply filtering to the name column. employeeDataSource.clearFilters(columnName: 'name'); if (value.isNotEmpty) { employeeDataSource.addFilter( 'name', FilterCondition( type: FilterType.contains, filterBehavior: FilterBehavior.stringDataType, value: value)); } // To refresh the DataGrid. employeeDataSource.updateDataGriDataSource(); // Clear selection. _dataGridController.selectedIndex = -1; // Reset to the first filtered row. _currentSelectedIndex = 0; _dataGridController.selectedIndex = _currentSelectedIndex; _dataGridController.scrollToRow(_currentSelectedIndex.toDouble()); }, inputFormatters: [ FilteringTextInputFormatter.deny(RegExp("[\n\r]")), ], ), ), Expanded( child: SfDataGrid( source: employeeDataSource, controller: _dataGridController, selectionMode: SelectionMode.single, columnWidthMode: ColumnWidthMode.fill, columns: [ GridColumn( columnName: 'id', label: Container( padding: const EdgeInsets.all(8.0), alignment: Alignment.center, child: const Text('ID'), ), ), GridColumn( columnName: 'name', label: Container( padding: const EdgeInsets.all(8.0), alignment: Alignment.center, child: const Text('Name'), ), ), GridColumn( columnName: 'designation', label: Container( padding: const EdgeInsets.all(8.0), alignment: Alignment.center, child: const Text('Designation'), ), ), ], ), ), ], ), ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Close'), ), ], ); } }

// Data model for the Employee class Employee { Employee(this.id, this.name, this.designation); final int id; final String name; final String designation; }

// DataSource class for DataGrid class EmployeeDataSource extends DataGridSource { EmployeeDataSource(this.employees) { dataGridRows = employees .map((e) => DataGridRow(cells: [ DataGridCell(columnName: 'id', value: e.id), DataGridCell(columnName: 'name', value: e.name), DataGridCell(columnName: 'designation', value: e.designation), ])) .toList(); }

List dataGridRows = []; List employees;

@override List get rows => dataGridRows;

@override DataGridRowAdapter buildRow(DataGridRow row) { return DataGridRowAdapter( cells: row.getCells().map((dataGridCell) { return Container( alignment: Alignment.center, padding: const EdgeInsets.all(8.0), child: Text(dataGridCell.value.toString()), ); }).toList(), ); }

void updateDataGriDataSource() { notifyListeners(); } }

// Sample data for employees (30 rows) List getEmployees() { return [ Employee(10001, 'James', 'Project Lead'), Employee(10002, 'Kathryn', 'Manager'), Employee(10003, 'Liam', 'Software Engineer'), Employee(10004, 'Michael', 'Designer'), Employee(10005, 'Martin', 'Developer'), Employee(10006, 'Newberry', 'Developer'), Employee(10007, 'Evelyn', 'Quality Assurance'), Employee(10008, 'Perry', 'Developer'), Employee(10009, 'Gable', 'Developer'), Employee(10010, 'Grimes', 'Developer'), Employee(10011, 'Sophia', 'Data Analyst'), Employee(10012, 'Isabella', 'UI/UX Designer'), Employee(10013, 'Jacob', 'Backend Developer'), Employee(10014, 'Mason', 'Frontend Developer'), Employee(10015, 'Olivia', 'Scrum Master'), Employee(10016, 'Emma', 'Project Coordinator'), Employee(10017, 'Ava', 'Business Analyst'), Employee(10018, 'William', 'DevOps Engineer'), Employee(10019, 'Jackson', 'Product Owner'), Employee(10020, 'Amelia', 'HR Specialist'), Employee(10021, 'Ethan', 'Database Administrator'), Employee(10022, 'Lucas', 'Security Analyst'), Employee(10023, 'Mia', 'Cloud Engineer'), Employee(10024, 'Elijah', 'Infrastructure Specialist'), Employee(10025, 'Harper', 'Technical Writer'), Employee(10026, 'Evelyn', 'Quality Assurance'), Employee(10027, 'Benjamin', 'Architect'), Employee(10028, 'Alexander', 'Developer'), Employee(10029, 'Sebastian', 'Junior Developer'), Employee(10030, 'Gabriel', 'Intern'), ]; }

I have made significant improvements to the code you provided. However, there’s one remaining issue: when I press the down arrow key, the grid scrolls each time, causing the previous row to become invisible. The scrolling should only occur when the selection reaches the last row, similar to how a standard focused GridView behaves.

abineshPalanisamy commented 1 month ago

Hi @abdulrehmananwar ,

Based on the information provided, we have modified the sample to meet your requirements. In the SfDataGrid, you can obtain the starting and ending indices of the visible rows using the DataGridController.getVisibleRowStartIndex and DataGridController.getVisibleRowEndIndex methods, respectively, by specifying the required RowRegion. We have restricted scrolling when the down arrow key is pressed, so scrolling will only occur when the selection reaches the last visible row. We have included a sample for your reference. Please review it for further details.

Regards, Abinesh P

abdulrehmananwar commented 1 month ago

Thanks for sharing the sample code. However, I'm using this in a Windows app, and when I press the up or down key once, it navigates twice instead of once. Can you help me fix this?

abdulrehmananwar commented 1 month ago

https://github.com/user-attachments/assets/ef8c488a-df62-4ed5-8982-8b6a898e6a2d

I noticed an issue: when I press the arrow key once, it behaves as if I've pressed it twice.

abineshPalanisamy commented 1 month ago

Hi @abdulrehmananwar ,

The issue you're encountering, where pressing the down arrow key triggers two actions instead of one, may be due to the KeyboardListener responding to both the key down and key up events. In Flutter, a single key press can generate multiple events: one when the key is pressed down and another when it's released. To address this, modify the event handler to check the type of key event being received, and ensure that you only respond to the key down event.

However, note that the KeyDownEvent does not fire continuously when a key is held down. Flutter triggers the KeyDownEvent only once for a single press. To detect continuous key presses, you also need to handle both KeyDownEvent and KeyRepeatEvent. We have included a sample for your reference. Please review it for further details.

Regards, Abinesh P

ashok-kuvaraja commented 3 weeks ago

Hi @abdulrehmananwar,

We suspect that the reported issue has been resolved at your end. Hence, we are closing this issue. If you need any further assistance, please reopen this. We are always happy to help.

Regards, Ashok K