ag-grid 中的键盘事件问题,react-select 作为弹出自定义单元格编辑器
Problem with keyboard event in ag-grid with react-select as a popup custom cell editor
我想使用 react-select 作为 ag-grid
的自定义单元格编辑器
可以下载源码here
npm install
npm start
我删除了所有 css 所以它看起来很普通,但它仍然可以工作。
这是我的 package.json
{
"name": "Test",
"version": "1.5.0",
"private": true,
"dependencies": {
"react": "16.8.1",
"react-dom": "16.8.1",
"react-select": "^2.4.1",
"react-scripts": "2.1.5",
"ag-grid-community": "20.1.0",
"ag-grid-react": "20.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"deploy": "npm run build",
"lint:check": "eslint . --ext=js,jsx; exit 0",
"lint:fix": "eslint . --ext=js,jsx --fix; exit 0",
"install:clean": "rm -rf node_modules/ && rm -rf package-lock.json && npm install && npm start"
},
"optionalDependencies": {
"@types/googlemaps": "3.30.16",
"@types/markerclustererplus": "2.1.33",
"ajv": "6.9.1",
"prettier": "1.16.4"
},
"devDependencies": {
"eslint-config-prettier": "4.0.0",
"eslint-plugin-prettier": "3.0.1"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
index.js
import React from "react";
import ReactDOM from "react-dom";
import Grid from './app/AgGrid'
const colDefs = [
{
headerName : "Test",
field : "test",
editor : "manyToOne",
editable : true,
suppressKeyboardEvent : function suppressEnter(params) {
let KEY_ENTER = 13;
let KEY_LEFT = 37;
let KEY_UP = 38;
let KEY_RIGHT = 39;
let KEY_DOWN = 40;
var event = params.event;
var key = event.which;
var editingKeys = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_ENTER];
var suppress = params.editing && editingKeys.indexOf(key) >= 0;
console.log("suppress "+suppress);
return suppress;
},
cellEditor : "reactSelectCellEditor",
},
]
ReactDOM.render(
<Grid
readOnly={false}
field={null}
columnDefs={colDefs}
editable={true}
editingMode={null}
rowData={[]}
/>
,
document.getElementById("root")
);
AgGrid.js
import React, { Component } from 'react'
import { AgGridReact } from 'ag-grid-react'
import 'ag-grid-community/dist/styles/ag-grid.css'
import ReactSelectCellEditor from './ReactSelectCellEditor'
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
columnDefs : props.columnDefs,
rowData : [],
isFormOpen : false,
editedData : {
data : [],
rowIndex : null,
columns : []
},
frameworkComponents:{
'reactSelectCellEditor': ReactSelectCellEditor,
},
}
}
updateRowData = (newData,index) => {
var unsavedData = this.state.rowData.map((row,i)=>{
if (i === index) {
return newData
}
else{
return row
}
})
this.setState({rowData:unsavedData})
}
onGridReady = params => {
}
createData = () => {
let fields = this.state.columnDefs.map(column=>{
return column.field
})
let newData = {}
fields.map(field=>{
if (field !== "id") {
newData[field] = ""
}
})
newData["delete"] = false
this.setState({rowData:[...this.state.rowData,newData]})
}
toggleModal = () => {
this.setState({isFormOpen:!this.state.isFormOpen})
}
setEditedData = (data) => {
console.log("editedData",data)
this.setState({editedData:data})
}
render() {
var defaultColDef = {
editable:this.state.editable,
sortable:false
}
var initColDefs = this.state.columnDefs.map(colDef=>{
return {
...colDef,
cellEditorParams:{
...colDef.cellEditorParams,
}
}
})
var renderedColumnDefs = []
if (this.props.editingMode === "inline") {
renderedColumnDefs = [
...initColDefs
]
}
else if (this.props.readOnly) {
renderedColumnDefs = [...initColDefs]
}
else{
defaultColDef = {
editable:false,
sortable:false
}
renderedColumnDefs = [
...initColDefs,
]
}
var renderedRowData = []
this.state.rowData.map(row=>{
if (row.delete !== true) {
renderedRowData.push(row)
}
})
return (
<React.Fragment>
<AgGridReact
defaultColDef = {defaultColDef}
columnDefs = {renderedColumnDefs}
rowData = {renderedRowData}
onGridReady = {this.onGridReady}
onCellValueChanged = {this.handleChange}
frameworkComponents = {this.state.frameworkComponents}
singleClickEdit = {true}
stopEditingWhenGridLosesFocus = {true}
reactNext={true}
/>
<div className="addButton" style={{padding:'5px', border: '1px solid black'}} onClick={()=>this.createData()}>Add</div>
</React.Fragment>
);
}
}
export default Grid
和ReactSelectCellEditor.js
import React, { Component } from 'react'
import CreatableSelect from "react-select/lib/Creatable"
import { components } from 'react-select'
import ReactDOM from 'react-dom'
const colourOptions = [
{ value: 'red', label: 'Red' },
{ value: 'blue', label: 'Blue' },
{ value: 'yellow', label: 'Yellow' }
]
class ReactSelectCellEditor extends Component {
constructor(props) {
super(props);
this.state = {
value : null
}
this.handleChange = this.handleChange.bind(this)
}
componentDidMount() {
}
handleCreateOption = (inputValue:any) => {
}
handleChange(selected){
this.setState({
value : selected.value
})
}
afterGuiAttached(param) {
let self = this;
this.Ref.addEventListener('keydown', function (event) {
// let key = event.which || event.keyCode
// if (key === 37 || key === 38 || key === 39 || key === 40 || key === 13) {
// event.stopPropagation();
// console.log("onKeyDown "+key);
// }
});
this.SelectRef.focus()
}
formatCreate = (inputValue) => {
return (<p> Add: {inputValue}</p>);
};
isPopup(){
return true
}
getValue() {
return this.state.value
}
render() {
return (
<div ref = { ref => { this.Ref = ref }} style={{width: '200px'}}>
<CreatableSelect
onChange = {this.handleChange}
options = {colourOptions}
formatCreateLabel = {this.formatCreate}
createOptionPosition = {"first"}
ref = { ref => { this.SelectRef = ref }}
/>
</div>
);
}
}
export default ReactSelectCellEditor
如您所见,弹出 ag-grid 和 之间的简单裸骨组合实现(不在单元格中)react-select.
它工作正常,除了 ag-grid 的默认导航键事件阻碍 react-select 完美工作。
现在我已经在 ag-grid documentation of keyboard navigation while editing
中阅读了有关此问题的解决方案
事情是这样的:
首先,我尝试了解决方案 1,即停止我的自定义单元格编辑器的事件传播。它有效,但奇怪的是 虽然 我停止了 react-select 的 容器 中的事件冒泡,而不是 react-select 本身,正如您从 ReactSelectCellEditor.js 中注释掉的脚本中看到的那样,React Select Key Event 也以某种方式停止执行。我不知道为什么当我停止 parent 元素中的事件传播时,作为 div 的 child 元素的反应 select 事件停止执行。难道事件不应该从 child 上升到 parent 而不是相反吗?所以第一个解决方案是不行的。
至于第二种解决方案,奇怪的是 ag-grid 本身。正如文档所说,ag-grid 允许我们通过从列定义中为 suppressKeyboardEvent 定义一个函数来拦截按键事件。如果编辑模式是 not popup,它会起作用。但就我而言,我使用弹出模式,在弹出模式下,我发现在编辑时没有触发 suppressKeyboardEvent,甚至根本没有调用它。
现在我陷入了僵局。在第一个解决方案中,react-select 表现得很奇怪,而在第二个解决方案中,ag-grid 表现得很奇怪。我该如何解决?
额外查找
通过互联网搜索后,React Select 似乎使用了 SyntheticKeyboardEvent
,如果我没记错的话,它会在事件已经经过第一个 capture/bubbling 循环之后执行DOM 树。所以如果我停止传播 SyntheticKeyboardEvent
将不会被触发。但是如果我不停止使用本机事件侦听器的传播 ag-grid 将触发其默认导航功能并终止 react-select 组件。现在这是给我的吗?死胡同?
让我警告你这个解决方案是一种 hack 和反模式。正如您所说,react select 使用 SyntheticKeyboardEvent 并等待本机事件通过 DOM 树的循环。但是有一种方法可以让你通过修改代码来调用 react select 功能,这将允许你从其他组件访问 react select 组件的方法,这就是为什么这是一个反模式解决方案。
从 colDef
中删除 suppressKeyboardEvent
从 node_modules/react-select/src/Creatable.js
复制 CreatableSelect
组件
将这些行添加到复制的组件中以通过 onRef 获取访问权限
componentDidMount() {
this.props.onRef(this)
}
componentWillUnmount() {
this.props.onRef(undefined)
}
通过在copy
中定义这些来传递reactselect的控制方法
focusOption(param) {
this.select.focusOption(param);
}
selectOption(param) {
this.select.selectOption(param);
}
然后在渲染组件时添加 onRef = { ref => { this.SelectRef = ref }}
现在您将能够在停止传播之前调用这些来模拟控件
this.SelectRef.focusOption('up');
this.SelectRef.focusOption('down');
您可以通过编写这样的方法来访问对 select 的状态做出反应
focusedOption() {
return this.select.state.focusedOption;
}
参考node_modules/react-select/src/Select.js 1142行
完成剩下的模拟
我想使用 react-select 作为 ag-grid
的自定义单元格编辑器可以下载源码here
npm install
npm start
我删除了所有 css 所以它看起来很普通,但它仍然可以工作。
这是我的 package.json
{
"name": "Test",
"version": "1.5.0",
"private": true,
"dependencies": {
"react": "16.8.1",
"react-dom": "16.8.1",
"react-select": "^2.4.1",
"react-scripts": "2.1.5",
"ag-grid-community": "20.1.0",
"ag-grid-react": "20.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"deploy": "npm run build",
"lint:check": "eslint . --ext=js,jsx; exit 0",
"lint:fix": "eslint . --ext=js,jsx --fix; exit 0",
"install:clean": "rm -rf node_modules/ && rm -rf package-lock.json && npm install && npm start"
},
"optionalDependencies": {
"@types/googlemaps": "3.30.16",
"@types/markerclustererplus": "2.1.33",
"ajv": "6.9.1",
"prettier": "1.16.4"
},
"devDependencies": {
"eslint-config-prettier": "4.0.0",
"eslint-plugin-prettier": "3.0.1"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
index.js
import React from "react";
import ReactDOM from "react-dom";
import Grid from './app/AgGrid'
const colDefs = [
{
headerName : "Test",
field : "test",
editor : "manyToOne",
editable : true,
suppressKeyboardEvent : function suppressEnter(params) {
let KEY_ENTER = 13;
let KEY_LEFT = 37;
let KEY_UP = 38;
let KEY_RIGHT = 39;
let KEY_DOWN = 40;
var event = params.event;
var key = event.which;
var editingKeys = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_ENTER];
var suppress = params.editing && editingKeys.indexOf(key) >= 0;
console.log("suppress "+suppress);
return suppress;
},
cellEditor : "reactSelectCellEditor",
},
]
ReactDOM.render(
<Grid
readOnly={false}
field={null}
columnDefs={colDefs}
editable={true}
editingMode={null}
rowData={[]}
/>
,
document.getElementById("root")
);
AgGrid.js
import React, { Component } from 'react'
import { AgGridReact } from 'ag-grid-react'
import 'ag-grid-community/dist/styles/ag-grid.css'
import ReactSelectCellEditor from './ReactSelectCellEditor'
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
columnDefs : props.columnDefs,
rowData : [],
isFormOpen : false,
editedData : {
data : [],
rowIndex : null,
columns : []
},
frameworkComponents:{
'reactSelectCellEditor': ReactSelectCellEditor,
},
}
}
updateRowData = (newData,index) => {
var unsavedData = this.state.rowData.map((row,i)=>{
if (i === index) {
return newData
}
else{
return row
}
})
this.setState({rowData:unsavedData})
}
onGridReady = params => {
}
createData = () => {
let fields = this.state.columnDefs.map(column=>{
return column.field
})
let newData = {}
fields.map(field=>{
if (field !== "id") {
newData[field] = ""
}
})
newData["delete"] = false
this.setState({rowData:[...this.state.rowData,newData]})
}
toggleModal = () => {
this.setState({isFormOpen:!this.state.isFormOpen})
}
setEditedData = (data) => {
console.log("editedData",data)
this.setState({editedData:data})
}
render() {
var defaultColDef = {
editable:this.state.editable,
sortable:false
}
var initColDefs = this.state.columnDefs.map(colDef=>{
return {
...colDef,
cellEditorParams:{
...colDef.cellEditorParams,
}
}
})
var renderedColumnDefs = []
if (this.props.editingMode === "inline") {
renderedColumnDefs = [
...initColDefs
]
}
else if (this.props.readOnly) {
renderedColumnDefs = [...initColDefs]
}
else{
defaultColDef = {
editable:false,
sortable:false
}
renderedColumnDefs = [
...initColDefs,
]
}
var renderedRowData = []
this.state.rowData.map(row=>{
if (row.delete !== true) {
renderedRowData.push(row)
}
})
return (
<React.Fragment>
<AgGridReact
defaultColDef = {defaultColDef}
columnDefs = {renderedColumnDefs}
rowData = {renderedRowData}
onGridReady = {this.onGridReady}
onCellValueChanged = {this.handleChange}
frameworkComponents = {this.state.frameworkComponents}
singleClickEdit = {true}
stopEditingWhenGridLosesFocus = {true}
reactNext={true}
/>
<div className="addButton" style={{padding:'5px', border: '1px solid black'}} onClick={()=>this.createData()}>Add</div>
</React.Fragment>
);
}
}
export default Grid
和ReactSelectCellEditor.js
import React, { Component } from 'react'
import CreatableSelect from "react-select/lib/Creatable"
import { components } from 'react-select'
import ReactDOM from 'react-dom'
const colourOptions = [
{ value: 'red', label: 'Red' },
{ value: 'blue', label: 'Blue' },
{ value: 'yellow', label: 'Yellow' }
]
class ReactSelectCellEditor extends Component {
constructor(props) {
super(props);
this.state = {
value : null
}
this.handleChange = this.handleChange.bind(this)
}
componentDidMount() {
}
handleCreateOption = (inputValue:any) => {
}
handleChange(selected){
this.setState({
value : selected.value
})
}
afterGuiAttached(param) {
let self = this;
this.Ref.addEventListener('keydown', function (event) {
// let key = event.which || event.keyCode
// if (key === 37 || key === 38 || key === 39 || key === 40 || key === 13) {
// event.stopPropagation();
// console.log("onKeyDown "+key);
// }
});
this.SelectRef.focus()
}
formatCreate = (inputValue) => {
return (<p> Add: {inputValue}</p>);
};
isPopup(){
return true
}
getValue() {
return this.state.value
}
render() {
return (
<div ref = { ref => { this.Ref = ref }} style={{width: '200px'}}>
<CreatableSelect
onChange = {this.handleChange}
options = {colourOptions}
formatCreateLabel = {this.formatCreate}
createOptionPosition = {"first"}
ref = { ref => { this.SelectRef = ref }}
/>
</div>
);
}
}
export default ReactSelectCellEditor
如您所见,弹出 ag-grid 和 之间的简单裸骨组合实现(不在单元格中)react-select.
它工作正常,除了 ag-grid 的默认导航键事件阻碍 react-select 完美工作。
现在我已经在 ag-grid documentation of keyboard navigation while editing
中阅读了有关此问题的解决方案事情是这样的:
首先,我尝试了解决方案 1,即停止我的自定义单元格编辑器的事件传播。它有效,但奇怪的是 虽然 我停止了 react-select 的 容器 中的事件冒泡,而不是 react-select 本身,正如您从 ReactSelectCellEditor.js 中注释掉的脚本中看到的那样,React Select Key Event 也以某种方式停止执行。我不知道为什么当我停止 parent 元素中的事件传播时,作为 div 的 child 元素的反应 select 事件停止执行。难道事件不应该从 child 上升到 parent 而不是相反吗?所以第一个解决方案是不行的。
至于第二种解决方案,奇怪的是 ag-grid 本身。正如文档所说,ag-grid 允许我们通过从列定义中为 suppressKeyboardEvent 定义一个函数来拦截按键事件。如果编辑模式是 not popup,它会起作用。但就我而言,我使用弹出模式,在弹出模式下,我发现在编辑时没有触发 suppressKeyboardEvent,甚至根本没有调用它。
现在我陷入了僵局。在第一个解决方案中,react-select 表现得很奇怪,而在第二个解决方案中,ag-grid 表现得很奇怪。我该如何解决?
额外查找
通过互联网搜索后,React Select 似乎使用了 SyntheticKeyboardEvent
,如果我没记错的话,它会在事件已经经过第一个 capture/bubbling 循环之后执行DOM 树。所以如果我停止传播 SyntheticKeyboardEvent
将不会被触发。但是如果我不停止使用本机事件侦听器的传播 ag-grid 将触发其默认导航功能并终止 react-select 组件。现在这是给我的吗?死胡同?
让我警告你这个解决方案是一种 hack 和反模式。正如您所说,react select 使用 SyntheticKeyboardEvent 并等待本机事件通过 DOM 树的循环。但是有一种方法可以让你通过修改代码来调用 react select 功能,这将允许你从其他组件访问 react select 组件的方法,这就是为什么这是一个反模式解决方案。
从 colDef
中删除 从 node_modules/react-select/src/Creatable.js
复制 将这些行添加到复制的组件中以通过 onRef 获取访问权限
componentDidMount() { this.props.onRef(this) } componentWillUnmount() { this.props.onRef(undefined) }
通过在copy
中定义这些来传递reactselect的控制方法focusOption(param) { this.select.focusOption(param); } selectOption(param) { this.select.selectOption(param); }
然后在渲染组件时添加
onRef = { ref => { this.SelectRef = ref }}
现在您将能够在停止传播之前调用这些来模拟控件
this.SelectRef.focusOption('up'); this.SelectRef.focusOption('down');
您可以通过编写这样的方法来访问对 select 的状态做出反应
focusedOption() { return this.select.state.focusedOption; }
参考node_modules/react-select/src/Select.js 1142行
完成剩下的模拟
suppressKeyboardEvent
CreatableSelect
组件